diff --git a/.github/actions/init/action.yml b/.github/actions/init/action.yml index 69bb346b..f82d4fcc 100644 --- a/.github/actions/init/action.yml +++ b/.github/actions/init/action.yml @@ -14,9 +14,14 @@ runs: - name: Free up space on runner shell: bash run: | - sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/.ghcup + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/local/share/boost + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/share/swift sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Rust Cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 35a0766f..ee041702 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,9 +85,33 @@ jobs: - name: Run integration tests run: cargo test --release --locked --package integration-tests + api-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: "./.github/actions/init" + + - name: Run tests + working-directory: pop-api + run: cargo test --release --all-features + + api-integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: "./.github/actions/init" + + - name: Run integration tests + working-directory: pop-api/integration-tests + run: cargo test --release + coverage: needs: lint runs-on: ubuntu-latest + env: + RUSTFLAGS: "-C debuginfo=0" steps: - uses: actions/checkout@v4 @@ -104,4 +128,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: codecov.json - fail_ci_if_error: true \ No newline at end of file + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 52e0800a..b99b862b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ .idea # Binaries -**/bin/ \ No newline at end of file +**/bin/ + +# Cargo.lock +**/Cargo.lock \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2e7c7cbc..5f64fe92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1065,15 +1065,47 @@ dependencies = [ ] [[package]] -name = "bounded-collections" -version = "0.1.9" +name = "bollard" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca548b6163b872067dc5eb82fd130c56881435e30367d2073594a3d9744120dd" +checksum = "0aed08d3adb6ebe0eff737115056652670ae290f177759aac19c30456135f94c" dependencies = [ + "base64 0.22.1", + "bollard-stubs", + "bytes", + "futures-core", + "futures-util", + "hex", + "http 1.1.0", + "http-body-util", + "hyper 1.4.1", + "hyper-named-pipe", + "hyper-util", + "hyperlocal-next", "log", - "parity-scale-codec", - "scale-info", + "pin-project-lite", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tower-service", + "url", + "winapi", +] + +[[package]] +name = "bollard-stubs" +version = "1.44.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709d9aa1c37abb89d40f19f5d0ad6f0d88cb1581264e571c9350fc5bb89cf1c5" +dependencies = [ "serde", + "serde_repr", + "serde_with", ] [[package]] @@ -1499,6 +1531,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver 1.0.23", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.0.99" @@ -1590,8 +1636,9 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1745,6 +1792,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -1838,6 +1895,60 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" +[[package]] +name = "contract-build" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d30f629d8cb26692c4e5f4155e8ab37649f1b2fd0abab64a4c1b3c95c9bcf3df" +dependencies = [ + "anyhow", + "blake2 0.10.6", + "bollard", + "cargo_metadata 0.18.1", + "clap", + "colored", + "contract-metadata", + "crossterm", + "duct", + "heck 0.5.0", + "hex", + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "regex", + "rustc_version", + "semver 1.0.23", + "serde", + "serde_json", + "strum 0.26.2", + "tempfile", + "term_size", + "tokio", + "tokio-stream", + "toml 0.8.14", + "tracing", + "url", + "uzers", + "walkdir", + "wasm-opt", + "which 6.0.3", + "zip", +] + +[[package]] +name = "contract-metadata" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd54ed69476dc2076d6ebda17351babe1c2f33751170877f66719e8129078d3a" +dependencies = [ + "anyhow", + "impl-serde", + "semver 1.0.23", + "serde", + "serde_json", + "url", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -2053,6 +2164,31 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags 2.5.0", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.3", + "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.2" @@ -2467,7 +2603,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0adf5409618b21e754fef0ac70f257878d22d61c48fdeefcab666835dcb8e0f0" dependencies = [ - "bounded-collections 0.2.0", + "bounded-collections", "bp-xcm-bridge-hub-router", "cumulus-primitives-core", "frame-benchmarking", @@ -2900,6 +3036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -3082,6 +3219,18 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" +[[package]] +name = "duct" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ab5718d1224b63252cd0c6f74f6480f9ffeb117438a2e0f5cf6d9a4798929c" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + [[package]] name = "dyn-clonable" version = "0.9.0" @@ -3254,6 +3403,26 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "enum-iterator" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "enumflags2" version = "0.7.10" @@ -4589,6 +4758,22 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-named-pipe" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" +dependencies = [ + "hex", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", + "winapi", ] [[package]] @@ -4614,14 +4799,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", "hyper 1.4.1", "pin-project-lite", + "socket2 0.5.7", "tokio", "tower", "tower-service", + "tracing", +] + +[[package]] +name = "hyperlocal-next" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf569d43fa9848e510358c07b80f4adf34084ddc28c6a4a651ee8474c070dcc" +dependencies = [ + "hex", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] @@ -4933,6 +5136,7 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -5376,7 +5580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -6303,6 +6507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.48.0", ] @@ -7020,12 +7225,41 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_pipe" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "pallet-api" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "pop-chain-extension", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-asset-conversion" version = "18.0.0" @@ -8442,7 +8676,7 @@ version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7409458b7fedc5c7d46459da154ccc2dc22a843ce08e8ab6c1743ef5cf972c" dependencies = [ - "bounded-collections 0.2.0", + "bounded-collections", "frame-benchmarking", "frame-support", "frame-system", @@ -8718,7 +8952,7 @@ dependencies = [ "libc", "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -9671,7 +9905,7 @@ version = "13.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61070d0ff28f596890def0e0d03c231860796130b2a43e293106fa86a50c9a9" dependencies = [ - "bounded-collections 0.2.0", + "bounded-collections", "derive_more", "parity-scale-codec", "polkadot-core-primitives", @@ -10267,6 +10501,28 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "pop-chain-extension" +version = "0.1.0" +dependencies = [ + "contract-build", + "env_logger 0.11.5", + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "log", + "pallet-balances", + "pallet-contracts", + "pallet-timestamp", + "parity-scale-codec", + "rand", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pop-node" version = "0.2.0-alpha" @@ -10337,7 +10593,7 @@ dependencies = [ name = "pop-primitives" version = "0.0.0" dependencies = [ - "bounded-collections 0.1.9", + "enum-iterator", "parity-scale-codec", "scale-info", ] @@ -10381,6 +10637,7 @@ dependencies = [ "hex", "hex-literal", "log", + "pallet-api", "pallet-assets", "pallet-aura", "pallet-authorship", @@ -10406,6 +10663,7 @@ dependencies = [ "parity-scale-codec", "polkadot-parachain-primitives", "polkadot-runtime-common", + "pop-chain-extension", "pop-primitives", "pop-runtime-common", "scale-info", @@ -10831,7 +11089,7 @@ dependencies = [ "regex", "syn 1.0.109", "tempfile", - "which", + "which 4.4.2", ] [[package]] @@ -13325,6 +13583,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "serde_spanned" version = "0.6.6" @@ -13334,6 +13603,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "time", +] + [[package]] name = "serdect" version = "0.2.0" @@ -13432,12 +13730,43 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shared_child" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fa9338aed9a1df411814a5b2252f7cd206c55ae9bf2fa763f8de84603aa60c" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "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", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -14051,7 +14380,7 @@ dependencies = [ "array-bytes", "bitflags 1.3.2", "blake2 0.10.6", - "bounded-collections 0.2.0", + "bounded-collections", "bs58 0.5.1", "dyn-clonable", "ed25519-zebra", @@ -14598,7 +14927,7 @@ version = "31.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93cdaf72a1dad537bbb130ba4d47307ebe5170405280ed1aa31fa712718a400e" dependencies = [ - "bounded-collections 0.2.0", + "bounded-collections", "parity-scale-codec", "scale-info", "serde", @@ -14704,7 +15033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b7b5f531c6bf9629514ef8e5fda0e9e80dd84516957f710940d0e01d3fb36c" dependencies = [ "array-bytes", - "bounded-collections 0.2.0", + "bounded-collections", "derivative", "environmental", "impl-trait-for-tuples", @@ -14952,7 +15281,7 @@ checksum = "7dc993ad871b63fbba60362f3ea86583f5e7e1256e8fdcb3b5b249c9ead354bf" dependencies = [ "array-bytes", "build-helper", - "cargo_metadata", + "cargo_metadata 0.15.4", "console", "filetime", "frame-metadata", @@ -15099,6 +15428,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -15872,6 +16211,7 @@ dependencies = [ "form_urlencoded", "idna 1.0.0", "percent-encoding", + "serde", ] [[package]] @@ -15898,6 +16238,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uzers" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +dependencies = [ + "libc", + "log", +] + [[package]] name = "valuable" version = "0.1.0" @@ -16573,6 +16923,18 @@ dependencies = [ "rustix 0.38.34", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.34", + "winsafe", +] + [[package]] name = "wide" version = "0.7.24" @@ -16678,7 +17040,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -16713,18 +17084,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -16741,9 +17112,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -16759,9 +17130,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -16777,15 +17148,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -16801,9 +17172,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -16819,9 +17190,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -16837,9 +17208,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -16855,9 +17226,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -16887,6 +17258,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "write16" version = "1.0.0" @@ -17163,6 +17540,17 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index cc4fa5b1..0c44755a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,11 @@ license = "Unlicense" repository = "https://github.com/r0gue-io/pop-node/" [workspace] -exclude = [ "pop-api", "tests/contracts" ] +exclude = [ "extension/contract", "pop-api", "tests/contracts" ] members = [ "integration-tests", "node", + "pallets/*", "primitives", "runtime/devnet", "runtime/mainnet", @@ -32,10 +33,16 @@ clap = { version = "4.4.18", features = [ "derive" ] } codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [ "derive", ] } +contract-build = "4.1.1" +enumflags2 = "0.7.9" +env_logger = "0.11.5" futures = "0.3.28" +hex = "0.4.3" hex-literal = "0.4.1" +impl-trait-for-tuples = "0.2.2" jsonrpsee = { version = "0.23.2", features = [ "server" ] } log = { version = "0.4.21", default-features = false } +rand = "0.8.5" scale-info = { version = "2.11.1", default-features = false, features = [ "derive", ] } @@ -52,6 +59,8 @@ substrate-build-script-utils = "11.0.0" substrate-wasm-builder = "23.0.0" # Local +pallet-api = { path = "pallets/api", default-features = false } +pop-chain-extension = { path = "./extension", default-features = false } pop-primitives = { path = "./primitives", default-features = false } pop-runtime-common = { path = "runtime/common", default-features = false } pop-runtime-devnet = { path = "runtime/devnet", default-features = true } # default-features=true required for `-p pop-node` builds @@ -64,6 +73,7 @@ frame-benchmarking-cli = "40.0.0" frame-executive = { version = "36.0.0", default-features = false } frame-metadata-hash-extension = { version = "0.4.0", default-features = false } frame-support = { version = "36.0.0", default-features = false } +frame-support-procedural = { version = "=30.0.1", default-features = false } frame-system = { version = "36.1.0", default-features = false } frame-system-benchmarking = { version = "36.0.0", default-features = false } frame-system-rpc-runtime-api = { version = "33.0.0", default-features = false } diff --git a/extension/Cargo.toml b/extension/Cargo.toml new file mode 100644 index 00000000..6ca5e071 --- /dev/null +++ b/extension/Cargo.toml @@ -0,0 +1,60 @@ +[package] +authors.workspace = true +description.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "pop-chain-extension" +publish = false +repository.workspace = true +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +codec.workspace = true +impl-trait-for-tuples.workspace = true +log.workspace = true + +# Substrate +frame-support.workspace = true +frame-system.workspace = true +pallet-contracts.workspace = true +sp-core.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true + +[dev-dependencies] +contract-build.workspace = true +env_logger.workspace = true +pallet-balances.workspace = true +pallet-contracts.workspace = true +pallet-timestamp.workspace = true +rand.workspace = true +scale-info.workspace = true +sp-io.workspace = true + +[features] +default = [ "std" ] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-contracts/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "pallet-contracts/std", + "pallet-timestamp/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/extension/contract/Cargo.toml b/extension/contract/Cargo.toml new file mode 100755 index 00000000..52c56f66 --- /dev/null +++ b/extension/contract/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = [ "R0GUE " ] +description = "Simple contract for proxying a call to a chain extension." +edition = "2021" +name = "proxy" +version = "0.1.0" + +[dependencies] +ink = { version = "5.0.0", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +std = [ "ink/std" ] diff --git a/extension/contract/lib.rs b/extension/contract/lib.rs new file mode 100755 index 00000000..181ba28c --- /dev/null +++ b/extension/contract/lib.rs @@ -0,0 +1,51 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::{ + env::chain_extension::{ChainExtensionMethod, FromStatusCode}, + prelude::vec::Vec, +}; + +#[ink::contract] +mod contract { + use super::*; + + // Simple contract for proxying a call to a chain extension. + #[ink(storage)] + #[derive(Default)] + pub struct Proxy; + + impl Proxy { + #[ink(constructor)] + pub fn new() -> Self { + ink::env::debug_println!("Proxy::new()"); + Default::default() + } + + #[ink(message)] + pub fn call(&self, func_id: u32, input: Vec) -> Result, StatusCode> { + ink::env::debug_println!("Proxy::call() func_id={func_id}, input={input:?}"); + ChainExtensionMethod::build(func_id) + .input::>() + .output::, StatusCode>, true>() + .handle_error_code::() + .call(&input) + } + } +} + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct StatusCode(u32); +impl FromStatusCode for StatusCode { + fn from_status_code(status_code: u32) -> Result<(), Self> { + match status_code { + 0 => Ok(()), + _ => Err(StatusCode(status_code)), + } + } +} + +impl From for StatusCode { + fn from(_: ink::scale::Error) -> Self { + StatusCode(u32::MAX) + } +} diff --git a/extension/src/decoding.rs b/extension/src/decoding.rs new file mode 100644 index 00000000..7269a49a --- /dev/null +++ b/extension/src/decoding.rs @@ -0,0 +1,301 @@ +use sp_runtime::DispatchError; +use sp_std::vec::Vec; + +use super::*; + +/// Trait for decoding data read from contract memory. +pub trait Decode { + /// The output type to be decoded. + type Output: codec::Decode; + /// An optional processor, for performing any additional processing on data read from the + /// contract before decoding. + type Processor: Processor>; + /// The error to return if decoding fails. + type Error: Get; + + /// The log target. + const LOG_TARGET: &'static str; + + /// Decodes data read from contract memory. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn decode(env: &mut E) -> Result; +} + +/// Trait for processing a value based on additional information available from the environment. +pub trait Processor { + /// The type of value to be processed. + type Value; + + /// The log target. + const LOG_TARGET: &'static str; + + /// Processes the provided value. + /// + /// # Parameters + /// - `value` - The value to be processed. + /// - `env` - The current execution environment. + fn process(value: Self::Value, env: &impl Environment) -> Self::Value; +} + +/// Default processor implementation which just passes through the value unchanged. +pub struct Identity(PhantomData); +impl Processor for Identity { + type Value = Value; + + const LOG_TARGET: &'static str = ""; + + fn process(value: Self::Value, _env: &impl Environment) -> Self::Value { + value + } +} + +/// Default implementation for decoding data read from contract memory. +pub struct Decodes>, L = ()>(PhantomData<(O, W, E, P, L)>); +impl< + Output: codec::Decode, + Weight: WeightInfo, + Error: Get, + ValueProcessor: Processor>, + Logger: LogTarget, + > Decode for Decodes +{ + type Error = Error; + type Output = Output; + type Processor = ValueProcessor; + + const LOG_TARGET: &'static str = Logger::LOG_TARGET; + + /// Decodes data read from contract memory. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn decode(env: &mut E) -> Result { + // Charge appropriate weight for copying from contract, based on input length, prior to + // decoding. reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=CopyToContract(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_return(len)%2C + let len = env.in_len(); + let weight = Weight::seal_return(len); + let charged = env.charge_weight(weight)?; + log::debug!(target: Self::LOG_TARGET, "pre-decode weight charged: len={len}, weight={weight}, charged={charged:?}"); + // Read encoded input supplied by contract for buffer. + let mut input = env.read(len)?; + log::debug!(target: Self::LOG_TARGET, "input read: input={input:?}"); + // Perform any additional processing required. Any implementation is expected to charge + // weight as appropriate. + input = Self::Processor::process(input, env); + // Finally decode and return. + Output::decode(&mut &input[..]).map_err(|_| { + log::error!(target: Self::LOG_TARGET, "decoding failed: unable to decode input into output type. input={input:?}"); + Error::get() + }) + } +} + +/// Error to be returned when decoding fails. +pub struct DecodingFailed(PhantomData); +impl Get for DecodingFailed { + fn get() -> DispatchError { + pallet_contracts::Error::::DecodingFailed.into() + } +} + +#[cfg(test)] +mod tests { + use codec::{Decode as OriginalDecode, Encode}; + use frame_support::assert_ok; + + use super::*; + use crate::{ + extension::read_from_buffer_weight, + mock::{MockEnvironment, RemoveFirstByte, Test}, + }; + + type EnumDecodes = Decodes, DecodingFailed>; + + #[test] + fn identity_processor_works() { + let env = MockEnvironment::default(); + assert_eq!(Identity::process(42, &env), 42); + assert_eq!(Identity::process(vec![0, 1, 2, 3, 4], &env), vec![0, 1, 2, 3, 4]); + } + + #[test] + fn remove_first_byte_processor_works() { + let env = MockEnvironment::default(); + let result = RemoveFirstByte::process(vec![0, 1, 2, 3, 4], &env); + assert_eq!(result, vec![1, 2, 3, 4]) + } + + #[test] + fn decode_works() { + test_cases().into_iter().for_each(|t| { + let (input, output) = (t.0, t.1); + println!("input: {:?} -> output: {:?}", input, output); + let mut env = MockEnvironment::new(0, input); + // Decode `input` to `output`. + assert_eq!(EnumDecodes::decode(&mut env), Ok(output)); + }); + } + + #[test] + fn decode_charges_weight() { + test_cases().into_iter().for_each(|t| { + let (input, output) = (t.0, t.1); + println!("input: {:?} -> output: {:?}", input, output); + let mut env = MockEnvironment::new(0, input.clone()); + // Decode `input` to `output`. + assert_ok!(EnumDecodes::decode(&mut env)); + // Decode charges weight based on the length of the input. + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32)); + }); + } + + #[test] + fn decoding_failed_error_type_works() { + assert_eq!( + DecodingFailed::::get(), + pallet_contracts::Error::::DecodingFailed.into() + ) + } + + #[test] + fn decode_failure_returns_decoding_failed_error() { + let input = vec![100]; + let mut env = MockEnvironment::new(0, input.clone()); + let result = EnumDecodes::decode(&mut env); + assert_eq!(result, Err(pallet_contracts::Error::::DecodingFailed.into())); + } + + #[test] + fn decode_failure_charges_weight() { + let input = vec![100]; + let mut env = MockEnvironment::new(0, input.clone()); + assert!(EnumDecodes::decode(&mut env).is_err()); + // Decode charges weight based on the length of the input, also when decoding fails. + assert_eq!(env.charged(), ContractWeightsOf::::seal_return(input.len() as u32)); + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + enum ComprehensiveEnum { + SimpleVariant, + DataVariant(u8), + NamedFields { w: u8 }, + NestedEnum(InnerEnum), + OptionVariant(Option), + VecVariant(Vec), + TupleVariant(u8, u8), + NestedStructVariant(NestedStruct), + NestedEnumStructVariant(NestedEnumStruct), + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + enum InnerEnum { + A, + B { inner_data: u8 }, + C(u8), + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + struct NestedStruct { + x: u8, + y: u8, + } + + #[derive(Debug, Clone, PartialEq, Encode, OriginalDecode)] + struct NestedEnumStruct { + inner_enum: InnerEnum, + } + + // Creating a set of byte data input and the decoded enum variant. + fn test_cases() -> Vec<(Vec, ComprehensiveEnum)> { + vec![ + (vec![0, 0, 0, 0], ComprehensiveEnum::SimpleVariant), + (vec![1, 42, 0, 0], ComprehensiveEnum::DataVariant(42)), + (vec![2, 42, 0, 0], ComprehensiveEnum::NamedFields { w: 42 }), + (vec![3, 0, 0, 0], ComprehensiveEnum::NestedEnum(InnerEnum::A)), + (vec![3, 1, 42, 0], ComprehensiveEnum::NestedEnum(InnerEnum::B { inner_data: 42 })), + (vec![3, 2, 42, 0], ComprehensiveEnum::NestedEnum(InnerEnum::C(42))), + (vec![4, 1, 42, 0], ComprehensiveEnum::OptionVariant(Some(42))), + (vec![4, 0, 0, 0], ComprehensiveEnum::OptionVariant(None)), + (vec![5, 12, 1, 2, 3], ComprehensiveEnum::VecVariant(vec![1, 2, 3])), + (vec![5, 16, 1, 2, 3, 4], ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4])), + (vec![5, 20, 1, 2, 3, 4, 5], ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4, 5])), + (vec![6, 42, 43, 0], ComprehensiveEnum::TupleVariant(42, 43)), + ( + vec![7, 42, 43, 0], + ComprehensiveEnum::NestedStructVariant(NestedStruct { x: 42, y: 43 }), + ), + ( + vec![8, 1, 42, 0], + ComprehensiveEnum::NestedEnumStructVariant(NestedEnumStruct { + inner_enum: InnerEnum::B { inner_data: 42 }, + }), + ), + ] + } + + // Test showing all the different type of variants and its encoding. + #[test] + fn encoding_of_enum() { + // Creating each possible variant for an enum. + let enum_simple = ComprehensiveEnum::SimpleVariant; + let enum_data = ComprehensiveEnum::DataVariant(42); + let enum_named = ComprehensiveEnum::NamedFields { w: 42 }; + let enum_nested = ComprehensiveEnum::NestedEnum(InnerEnum::B { inner_data: 42 }); + let enum_option = ComprehensiveEnum::OptionVariant(Some(42)); + let enum_vec = ComprehensiveEnum::VecVariant(vec![1, 2, 3, 4, 5]); + let enum_tuple = ComprehensiveEnum::TupleVariant(42, 42); + let enum_nested_struct = + ComprehensiveEnum::NestedStructVariant(NestedStruct { x: 42, y: 42 }); + let enum_nested_enum_struct = + ComprehensiveEnum::NestedEnumStructVariant(NestedEnumStruct { + inner_enum: InnerEnum::C(42), + }); + + // Encode and print each variant individually to see their encoded values. + println!("{:?} -> {:?}", enum_simple, enum_simple.encode()); + println!("{:?} -> {:?}", enum_data, enum_data.encode()); + println!("{:?} -> {:?}", enum_named, enum_named.encode()); + println!("{:?} -> {:?}", enum_nested, enum_nested.encode()); + println!("{:?} -> {:?}", enum_option, enum_option.encode()); + println!("{:?} -> {:?}", enum_vec, enum_vec.encode()); + println!("{:?} -> {:?}", enum_tuple, enum_tuple.encode()); + println!("{:?} -> {:?}", enum_nested_struct, enum_nested_struct.encode()); + println!("{:?} -> {:?}", enum_nested_enum_struct, enum_nested_enum_struct.encode()); + } + + #[test] + fn encoding_decoding_dispatch_error() { + use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError}; + + let error = DispatchError::Module(ModuleError { + index: 255, + error: [2, 0, 0, 0], + message: Some("error message"), + }); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + // DispatchError::Module index is 3 + assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]); + assert_eq!( + decoded, + // `message` is skipped for encoding. + DispatchError::Module(ModuleError { index: 255, error: [2, 0, 0, 0], message: None }) + ); + + // Example DispatchError::Token + let error = DispatchError::Token(TokenError::UnknownAsset); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![7, 4]); + assert_eq!(decoded, error); + + // Example DispatchError::Arithmetic + let error = DispatchError::Arithmetic(ArithmeticError::Overflow); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![8, 1]); + assert_eq!(decoded, error); + } +} diff --git a/extension/src/environment.rs b/extension/src/environment.rs new file mode 100644 index 00000000..dc3818e8 --- /dev/null +++ b/extension/src/environment.rs @@ -0,0 +1,196 @@ +use core::fmt::Debug; + +use frame_support::pallet_prelude::Weight; +use pallet_contracts::chain_extension::{BufInBufOutState, ChargedAmount, Result, State}; +use sp_std::vec::Vec; + +use crate::AccountIdOf; + +/// Provides access to the parameters passed to a chain extension and its execution environment. +/// +/// A wrapper trait for `pallet_contracts::chain_extension::Environment`. All comments have been +/// copied solely for consistent developer experience in line with the wrapped type. +pub trait Environment { + /// The account identifier type for the runtime. + type AccountId; + /// The charged weight type. + type ChargedAmount: Debug; + + /// The function id within the `id` passed by a contract. + /// + /// It returns the two least significant bytes of the `id` passed by a contract as the other + /// two bytes represent the chain extension itself (the code which is calling this function). + fn func_id(&self) -> u16; + + /// The chain extension id within the `id` passed by a contract. + /// + /// It returns the two most significant bytes of the `id` passed by a contract which represent + /// the chain extension itself (the code which is calling this function). + fn ext_id(&self) -> u16; + + /// Charge the passed `amount` of weight from the overall limit. + /// + /// It returns `Ok` when there the remaining weight budget is larger than the passed + /// `weight`. It returns `Err` otherwise. In this case the chain extension should + /// abort the execution and pass through the error. + /// + /// The returned value can be used to with [`Self::adjust_weight`]. Other than that + /// it has no purpose. + /// + /// # Note + /// + /// Weight is synonymous with gas in substrate. + fn charge_weight(&mut self, amount: Weight) -> Result; + + /// Adjust a previously charged amount down to its actual amount. + /// + /// This is when a maximum a priori amount was charged and then should be partially + /// refunded to match the actual amount. + fn adjust_weight(&mut self, charged: Self::ChargedAmount, actual_weight: Weight); + + /// Grants access to the execution environment of the current contract call. + /// + /// Consult the functions on the returned type before re-implementing those functions. + // TODO: improve the return type to &mut + fn ext(&mut self) -> impl Ext; +} + +/// A wrapper type for `pallet_contracts::chain_extension::Environment`. +pub(crate) struct Env<'a, 'b, E: pallet_contracts::chain_extension::Ext, S: State>( + pub(crate) pallet_contracts::chain_extension::Environment<'a, 'b, E, S>, +); + +impl<'a, 'b, E: pallet_contracts::chain_extension::Ext, S: State> Environment + for Env<'a, 'b, E, S> +{ + type AccountId = AccountIdOf; + type ChargedAmount = ChargedAmount; + + fn func_id(&self) -> u16 { + self.0.func_id() + } + + fn ext_id(&self) -> u16 { + self.0.ext_id() + } + + fn charge_weight(&mut self, amount: Weight) -> Result { + self.0.charge_weight(amount) + } + + fn adjust_weight(&mut self, charged: Self::ChargedAmount, actual_weight: Weight) { + self.0.adjust_weight(charged, actual_weight) + } + + fn ext(&mut self) -> impl Ext { + ExternalEnvironment(self.0.ext()) + } +} + +/// A state that uses a buffer as input. +/// +/// A wrapper trait for `pallet_contracts::chain_extension::BufIn` related function available on +/// `pallet_contracts::chain_extension::Environment`. All comments have been copied solely for +/// consistent developer experience in line with the wrapped type. +pub trait BufIn { + /// The length of the input as passed in as `input_len`. + /// + /// A chain extension would use this value to calculate the dynamic part of its + /// weight. For example a chain extension that calculates the hash of some passed in + /// bytes would use `in_len` to charge the costs of hashing that amount of bytes. + /// This also subsumes the act of copying those bytes as a benchmarks measures both. + fn in_len(&self) -> u32; + /// Reads `min(max_len, in_len)` from contract memory. + /// + /// This does **not** charge any weight. The caller must make sure that the an + /// appropriate amount of weight is charged **before** reading from contract memory. + /// The reason for that is that usually the costs for reading data and processing + /// said data cannot be separated in a benchmark. Therefore a chain extension would + /// charge the overall costs either using `max_len` (worst case approximation) or using + /// [`in_len()`](Self::in_len). + fn read(&self, max_len: u32) -> Result>; +} + +impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufIn for Env<'a, 'b, E, BufInBufOutState> { + fn in_len(&self) -> u32 { + self.0.in_len() + } + + fn read(&self, max_len: u32) -> Result> { + self.0.read(max_len) + } +} + +/// A state that uses a buffer as output. +/// +/// A wrapper trait for `pallet_contracts::chain_extension::BufOut` related function available on +/// `pallet_contracts::chain_extension::Environment`. All comments have been copied solely for +/// consistent developer experience in line with the wrapped type. +pub trait BufOut { + /// Write the supplied buffer to contract memory. + /// + /// If the contract supplied buffer is smaller than the passed `buffer` an `Err` is returned. + /// If `allow_skip` is set to true the contract is allowed to skip the copying of the buffer + /// by supplying the guard value of `pallet-contracts::SENTINEL` as `out_ptr`. The + /// `weight_per_byte` is only charged when the write actually happens and is not skipped or + /// failed due to a too small output buffer. + fn write( + &mut self, + buffer: &[u8], + allow_skip: bool, + weight_per_byte: Option, + ) -> Result<()>; +} + +impl<'a, 'b, E: pallet_contracts::chain_extension::Ext> BufOut + for Env<'a, 'b, E, BufInBufOutState> +{ + fn write( + &mut self, + buffer: &[u8], + allow_skip: bool, + weight_per_byte: Option, + ) -> Result<()> { + self.0.write(buffer, allow_skip, weight_per_byte) + } +} + +/// An interface that provides access to the external environment in which the smart-contract is +/// executed. +/// +/// This interface is specialized to an account of the executing code, so all operations are +/// implicitly performed on that account. +/// +/// A wrapper trait for `pallet_contracts::chain_extension::Ext`. All comments have been copied +/// solely for consistent developer experience in line with the wrapped type. +pub trait Ext { + /// The account identifier type for the runtime. + type AccountId; + + /// Returns a reference to the account id of the current contract. + fn address(&self) -> &Self::AccountId; +} + +impl Ext for () { + type AccountId = (); + + fn address(&self) -> &Self::AccountId { + &() + } +} + +/// A wrapper type for a type implementing `pallet_contracts::chain_extension::Ext`. +pub(crate) struct ExternalEnvironment<'a, T: pallet_contracts::chain_extension::Ext>(&'a mut T); + +impl<'a, E: pallet_contracts::chain_extension::Ext> Ext for ExternalEnvironment<'a, E> { + type AccountId = AccountIdOf; + + fn address(&self) -> &Self::AccountId { + self.0.address() + } +} + +#[test] +fn default_ext_works() { + assert_eq!(().address(), &()) +} diff --git a/extension/src/functions.rs b/extension/src/functions.rs new file mode 100644 index 00000000..73a5fdf9 --- /dev/null +++ b/extension/src/functions.rs @@ -0,0 +1,560 @@ +use core::fmt::Debug; + +use super::*; + +/// A chain extension function. +pub trait Function { + /// The configuration of the contracts module. + type Config: pallet_contracts::Config; + /// Optional error conversion. + type Error: ErrorConverter; + + /// Executes the function. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn execute( + env: &mut (impl Environment> + BufIn + BufOut), + ) -> Result; +} + +/// A function for dispatching a runtime call. +pub struct DispatchCall(PhantomData<(M, C, D, F, E, L)>); +impl< + Matcher: Matches, + Config: pallet_contracts::Config + + frame_system::Config< + RuntimeCall: GetDispatchInfo + Dispatchable, + >, + Decoder: Decode>>, + Filter: Contains> + 'static, + Error: ErrorConverter, + Logger: LogTarget, + > Function for DispatchCall +{ + /// The configuration of the contracts module. + type Config = Config; + /// Optional error conversion. + type Error = Error; + + /// Executes the function. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn execute( + env: &mut (impl Environment + BufIn), + ) -> Result { + // Decode runtime call. + let call = Decoder::decode(env)?.into(); + log::debug!(target: Logger::LOG_TARGET, "decoded: call={call:?}"); + // Charge weight before dispatch. + let dispatch_info = call.get_dispatch_info(); + log::debug!(target: Logger::LOG_TARGET, "pre-dispatch info: dispatch_info={dispatch_info:?}"); + let charged = env.charge_weight(dispatch_info.weight)?; + log::debug!(target: Logger::LOG_TARGET, "pre-dispatch weight charged: charged={charged:?}"); + // Contract is the origin by default. + let origin = RawOrigin::Signed(env.ext().address().clone()); + log::debug!(target: Logger::LOG_TARGET, "contract origin: origin={origin:?}"); + let mut origin: Config::RuntimeOrigin = origin.into(); + // Ensure call allowed. + origin.add_filter(Filter::contains); + // Dispatch call. + let result = call.dispatch(origin); + log::debug!(target: Logger::LOG_TARGET, "dispatched: result={result:?}"); + // Adjust weight. + let weight = frame_support::dispatch::extract_actual_weight(&result, &dispatch_info); + env.adjust_weight(charged, weight); + log::debug!(target: Logger::LOG_TARGET, "weight adjusted: weight={weight:?}"); + match result { + Ok(_) => Ok(Converging(0)), + Err(e) => Error::convert(e.error, env), + } + } +} + +impl Matches for DispatchCall { + fn matches(env: &impl Environment) -> bool { + M::matches(env) + } +} + +/// A function for reading runtime state. +pub struct ReadState::Result>, E = (), L = ()>( + PhantomData<(M, C, R, D, F, RC, E, L)>, +); +impl< + Matcher: Matches, + Config: pallet_contracts::Config, + Read: Readable + Debug, + Decoder: Decode>, + Filter: Contains, + ResultConverter: Converter>, Error = DispatchError>, + Error: ErrorConverter, + Logger: LogTarget, + > Function for ReadState +{ + /// The configuration of the contracts module. + type Config = Config; + /// Optional error conversion. + type Error = Error; + + /// Executes the function. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn execute(env: &mut (impl Environment + BufIn + BufOut)) -> Result { + // Decode runtime state read + let read = Decoder::decode(env)?.into(); + log::debug!(target: Logger::LOG_TARGET, "decoded: read={read:?}"); + // Charge weight before read + let weight = read.weight(); + let charged = env.charge_weight(weight)?; + log::trace!(target: Logger::LOG_TARGET, "pre-read weight charged: weight={weight}, charged={charged:?}"); + // Ensure read allowed + ensure!(Filter::contains(&read), frame_system::Error::::CallFiltered); + let result = read.read(); + log::debug!(target: Logger::LOG_TARGET, "read: result={result:?}"); + // Perform any final conversion. Any implementation is expected to charge weight as + // appropriate. + let result = ResultConverter::try_convert(result, env)?.into(); + log::debug!(target: Logger::LOG_TARGET, "converted: result={result:?}"); + // Charge appropriate weight for writing to contract, based on result length. + let weight = ContractWeightsOf::::seal_input(result.len() as u32); + let charged = env.charge_weight(weight)?; + log::trace!(target: Logger::LOG_TARGET, "return result to contract: weight={weight}, charged={charged:?}"); + env.write(&result, false, None)?; // weight charged above + Ok(Converging(0)) + } +} + +impl Matches for ReadState { + fn matches(env: &impl Environment) -> bool { + M::matches(env) + } +} + +/// Trait to be implemented for a type handling a read of runtime state. +pub trait Readable { + /// The corresponding type carrying the result of the runtime state read. + type Result: Debug; + + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. + fn weight(&self) -> Weight; + + /// Performs the read and returns the result. + fn read(self) -> Self::Result; +} + +/// Trait for fallible conversion of a value based on additional information available from the +/// environment. +pub trait Converter { + /// The error type returned when conversion fails. + type Error; + /// The type of value to be converted. + type Source; + /// The target type. + type Target; + + /// The log target. + const LOG_TARGET: &'static str; + + /// Converts the provided value. + /// + /// # Parameters + /// - `value` - The value to be converted. + /// - `env` - The current execution environment. + fn try_convert( + value: Self::Source, + env: &impl Environment, + ) -> core::result::Result; +} + +/// A default converter, for converting (encoding) from some type into a byte array. +pub struct DefaultConverter(PhantomData); +impl>> Converter for DefaultConverter { + /// The error type returned when conversion fails. + type Error = DispatchError; + /// The type of value to be converted. + type Source = T; + /// The target type. + type Target = Vec; + + const LOG_TARGET: &'static str = ""; + + fn try_convert( + value: Self::Source, + _env: &impl Environment, + ) -> core::result::Result { + Ok(value.into()) + } +} + +/// Trait for error conversion. +pub trait ErrorConverter { + /// The log target. + const LOG_TARGET: &'static str; + + /// Converts the provided error. + /// + /// # Parameters + /// - `error` - The error to be converted. + /// - `env` - The current execution environment. + fn convert(error: DispatchError, env: &impl Environment) -> Result; +} + +impl ErrorConverter for () { + const LOG_TARGET: &'static str = "pop-chain-extension::converters::error"; + + fn convert(error: DispatchError, _env: &impl Environment) -> Result { + Err(error) + } +} + +// Support tuples of at least one function (required for type resolution) and a maximum of ten. +#[impl_trait_for_tuples::impl_for_tuples(1, 10)] +#[tuple_types_custom_trait_bound(Function + Matches)] +impl Function for Tuple { + type Config = Runtime; + type Error = (); + + for_tuples!( where #( Tuple: Function )* ); + + fn execute( + env: &mut (impl Environment> + BufIn + BufOut), + ) -> Result { + // Attempts to match a specified extension/function identifier to its corresponding + // function, as configured by the runtime. + for_tuples!( #( + if Tuple::matches(env) { + return Tuple::execute(env) + } + )* ); + + // Otherwise returns error indicating an unmatched request. + log::error!("no function could be matched"); + Err(pallet_contracts::Error::::DecodingFailed.into()) + } +} + +#[cfg(test)] +mod tests { + use codec::Encode; + use frame_support::traits::{Everything, Nothing}; + use frame_system::Call; + use mock::{new_test_ext, Functions, MockEnvironment, RuntimeCall, RuntimeRead, Test}; + use sp_core::ConstU32; + + use super::*; + use crate::{ + extension::{read_from_buffer_weight, write_to_contract_weight}, + matching::WithFuncId, + mock::{Noop, NoopFuncId, INVALID_FUNC_ID}, + }; + + type FuncId = ConstU32<42>; + + enum AtLeastOneByte {} + impl Contains> for AtLeastOneByte { + fn contains(input: &Vec) -> bool { + input.len() > 0 + } + } + + enum LargerThan100 {} + impl Contains for LargerThan100 { + fn contains(input: &u8) -> bool { + *input > 100 + } + } + + enum MustBeEven {} + impl Contains for MustBeEven { + fn contains(input: &u8) -> bool { + *input % 2 == 0 + } + } + + #[test] + fn contains_works() { + fn contains, T>(input: T, expected: bool) { + assert_eq!(C::contains(&input), expected); + } + contains::(42, true); + contains::>(vec![1, 2, 3, 4], true); + contains::(42, false); + contains::>(vec![1, 2, 3, 4], false); + contains::>(vec![], false); + contains::>(vec![1], true); + contains::>(vec![1, 2, 3, 4], true); + contains::(100, false); + contains::(101, true); + contains::(100, true); + contains::(101, false); + } + + mod dispatch_call { + use super::*; + + type DispatchCall = DispatchCallWithFilter; + type DispatchCallWithFilter = super::DispatchCall< + WithFuncId, + Test, + Decodes, DecodingFailed>, + Filter, + >; + + #[test] + fn dispatch_call_filtering_works() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + let error = frame_system::Error::::CallFiltered.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(DispatchCallWithFilter::::execute(&mut env).err(), expected); + } + + #[test] + fn dispatch_call_filtered_charges_weight() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + assert!(DispatchCallWithFilter::::execute(&mut env).is_err()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + } + + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::remark_with_event { + remark: "pop".as_bytes().to_vec(), + }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + assert!(matches!(DispatchCall::execute(&mut env), Ok(Converging(0)))); + }) + } + + #[test] + fn dispatch_call_returns_error() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::set_code { code: "pop".as_bytes().to_vec() }); + let mut env = MockEnvironment::new(FuncId::get(), call.encode()); + let error = DispatchCall::execute(&mut env).err(); + let expected = + <() as ErrorConverter>::convert(DispatchError::BadOrigin, &env).err(); + assert_eq!(error, expected); + }) + } + + #[test] + fn dispatch_call_charges_weight() { + new_test_ext().execute_with(|| { + let call = RuntimeCall::System(Call::remark_with_event { + remark: "pop".as_bytes().to_vec(), + }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + assert!(DispatchCall::execute(&mut env).is_ok()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + }) + } + + #[test] + fn dispatch_call_adjusts_weight() { + let migrate_weight = ::WeightInfo::migrate(); + let migration_noop_weight = + ::WeightInfo::migration_noop(); + new_test_ext().execute_with(|| { + // Attempt to perform non-existent migration with additional weight limit specified. + let extra_weight = Weight::from_parts(123456789, 12345); + let weight_limit = migration_noop_weight.saturating_add(extra_weight); + let call = RuntimeCall::Contracts(pallet_contracts::Call::migrate { weight_limit }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_call.clone()); + let expected: DispatchError = + pallet_contracts::Error::::NoMigrationPerformed.into(); + assert_eq!(DispatchCall::execute(&mut env).err().unwrap(), expected); + // Ensure pre-dispatch weight is weight function + weight limit + assert_eq!(call.get_dispatch_info().weight, migrate_weight + weight_limit); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight - + extra_weight + ); + }) + } + + #[test] + fn dispatch_call_with_invalid_input_returns_error() { + // Invalid encoded runtime call. + let input = vec![0, 99]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(DispatchCall::execute(&mut env).err(), expected); + } + + #[test] + fn dispatch_call_with_invalid_input_charges_weight() { + // Invalid encoded runtime call. + let input = vec![0, 99]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + assert!(DispatchCall::execute(&mut env).is_err()); + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32,)); + } + } + + mod read_state { + use super::*; + use crate::mock::{RuntimeResult, UppercaseConverter}; + + type ReadState = ReadStateWithFilter; + type ReadStateWithFilter = super::ReadState< + WithFuncId, + Test, + RuntimeRead, + Decodes, DecodingFailed>, + Filter, + >; + type ReadStateWithResultConverter = super::ReadState< + WithFuncId, + Test, + RuntimeRead, + Decodes, DecodingFailed>, + Everything, + ResultConverter, + >; + + #[test] + fn read_state_filtering_works() { + let read = RuntimeRead::Ping; + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + let error = frame_system::Error::::CallFiltered.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(ReadStateWithFilter::::execute(&mut env).err(), expected); + } + + #[test] + fn read_state_filtered_charges_weight() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_read.clone()); + assert!(ReadStateWithFilter::::execute(&mut env).is_err()); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_read.len() as u32) + read.weight() + ); + } + + #[test] + fn read_state_works() { + let read = RuntimeRead::Ping; + let expected = "pop".as_bytes().encode(); + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + assert!(matches!(ReadState::execute(&mut env), Ok(Converging(0)))); + // Check if the contract environment buffer is written correctly. + assert_eq!(env.buffer, expected); + } + + #[test] + fn read_state_result_conversion_works() { + let read = RuntimeRead::Ping; + let expected = RuntimeResult::Pong("pop".to_string()); + let mut env = MockEnvironment::new(FuncId::get(), read.encode()); + assert!(matches!( + ReadStateWithResultConverter::::execute(&mut env), + Ok(Converging(0)) + )); + // Check if the contract environment buffer is written correctly. + assert_eq!(Ok(&env.buffer), UppercaseConverter::try_convert(expected, &env).as_ref()); + } + + #[test] + fn read_state_charges_weight() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let mut env = MockEnvironment::new(FuncId::get(), encoded_read.clone()); + assert!(ReadState::execute(&mut env).is_ok()); + let expected = "pop".as_bytes().encode(); + assert_eq!( + env.charged(), + read_from_buffer_weight(encoded_read.len() as u32) + + read.weight() + write_to_contract_weight(expected.len() as u32) + ); + } + + #[test] + fn read_state_with_invalid_input_returns_error() { + // Invalid encoded runtime state read. + let input = vec![0]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(ReadState::execute(&mut env).err(), expected); + } + + #[test] + fn read_state_with_invalid_input_charges_weight() { + // Invalid encoded runtime state read. + let input = vec![0]; + let mut env = MockEnvironment::new(FuncId::get(), input.clone()); + assert!(ReadState::execute(&mut env).is_err()); + assert_eq!(env.charged(), read_from_buffer_weight(input.len() as u32)); + } + } + + #[test] + fn execute_tuple_matches_and_executes_function() { + type Functions = (Noop, Test>,); + let mut env = MockEnvironment::new(NoopFuncId::get(), vec![]); + assert!(matches!(Functions::execute(&mut env), Ok(Converging(0)))); + } + + #[test] + fn execute_tuple_with_invalid_function_fails() { + let input = vec![]; + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + let error = pallet_contracts::Error::::DecodingFailed.into(); + let expected = <() as ErrorConverter>::convert(error, &mut env).err(); + assert_eq!(Functions::execute(&mut env).err(), expected); + } + + #[test] + fn execute_tuple_with_invalid_function_does_not_charge_weight() { + let input = vec![]; + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + assert!(Functions::execute(&mut env).is_err()); + // No weight charged as no function in the `Functions` tuple is matched to charge weight. + // See extension tests for extension call weight charges. + assert_eq!(env.charged(), Weight::default()); + } + + #[test] + fn default_error_conversion_works() { + let env = MockEnvironment::default(); + assert!(matches!( + <() as ErrorConverter>::convert(DispatchError::BadOrigin, &env), + Err(DispatchError::BadOrigin) + )); + } + + #[test] + fn default_conversion_works() { + let env = MockEnvironment::default(); + let source = "pop".to_string(); + assert_eq!( + DefaultConverter::try_convert(source.clone(), &env), + Ok(source.as_bytes().into()) + ); + } +} diff --git a/extension/src/lib.rs b/extension/src/lib.rs new file mode 100644 index 00000000..c2480c06 --- /dev/null +++ b/extension/src/lib.rs @@ -0,0 +1,230 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use core::marker::PhantomData; + +pub use decoding::{Decode, Decodes, DecodingFailed, Identity, Processor}; +pub use environment::{BufIn, BufOut, Environment, Ext}; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo, RawOrigin}, + ensure, + traits::{Contains, OriginTrait}, + weights::Weight, +}; +pub use functions::{ + Converter, DefaultConverter, DispatchCall, ErrorConverter, Function, ReadState, Readable, +}; +pub use matching::{Equals, FunctionId, Matches}; +pub use pallet_contracts::chain_extension::{Result, RetVal, State}; +use pallet_contracts::{ + chain_extension::{ChainExtension, InitState, RetVal::Converging}, + WeightInfo, +}; +use sp_core::Get; +use sp_runtime::{traits::Dispatchable, DispatchError}; +use sp_std::vec::Vec; + +mod decoding; +mod environment; +mod functions; +mod matching; +// Mock runtime/environment for unit/integration testing. +#[cfg(test)] +mod mock; +// Integration tests using proxy contract and mock runtime. +#[cfg(test)] +mod tests; + +type AccountIdOf = ::AccountId; +pub type ContractWeightsOf = ::WeightInfo; +type RuntimeCallOf = ::RuntimeCall; + +/// A configurable chain extension. +#[derive(Default)] +pub struct Extension(PhantomData); +impl ChainExtension for Extension +where + Runtime: pallet_contracts::Config + + frame_system::Config< + RuntimeCall: GetDispatchInfo + Dispatchable, + >, + Config: self::Config> + 'static, +{ + /// Call the chain extension logic. + /// + /// # Parameters + /// - `env`: Access to the remaining arguments and the execution environment. + fn call>( + &mut self, + env: pallet_contracts::chain_extension::Environment, + ) -> Result { + let mut env = environment::Env(env.buf_in_buf_out()); + self.call(&mut env) + } +} + +impl< + Runtime: pallet_contracts::Config, + Config: self::Config>, + > Extension +{ + fn call( + &mut self, + env: &mut (impl Environment + BufIn + BufOut), + ) -> Result { + log::trace!(target: Config::LOG_TARGET, "extension called"); + // Charge weight for making a call from a contract to the runtime. + // `debug_message` weight is a good approximation of the additional overhead of going from + // contract layer to substrate layer. reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=DebugMessage(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_debug_message(len)%2C + let len = env.in_len(); + let overhead = ContractWeightsOf::::seal_debug_message(len); + let charged = env.charge_weight(overhead)?; + log::debug!(target: Config::LOG_TARGET, "extension call weight charged: len={len}, weight={overhead}, charged={charged:?}"); + // Execute the function + Config::Functions::execute(env) + } +} + +/// Trait for configuration of the chain extension. +pub trait Config { + /// The function(s) available with the chain extension. + type Functions: Function; + + /// The log target. + const LOG_TARGET: &'static str; +} + +/// Trait to enable specification of a log target. +pub trait LogTarget { + /// The log target. + const LOG_TARGET: &'static str; +} + +impl LogTarget for () { + const LOG_TARGET: &'static str = "pop-chain-extension"; +} + +#[test] +fn default_log_target_works() { + assert!(matches!(<() as LogTarget>::LOG_TARGET, "pop-chain-extension")); +} + +#[cfg(test)] +mod extension { + use codec::Encode; + use frame_system::Call; + + use super::*; + use crate::mock::{ + new_test_ext, DispatchExtFuncId, MockEnvironment, NoopFuncId, ReadExtFuncId, RuntimeCall, + RuntimeRead, Test, INVALID_FUNC_ID, + }; + + #[test] + fn call_works() { + let input = vec![2, 2]; + let mut env = MockEnvironment::new(NoopFuncId::get(), input.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!(env.charged(), overhead_weight(input.len() as u32)) + } + + #[test] + fn calling_unknown_function_fails() { + let input = vec![2, 2]; + // No function registered for id 0. + let mut env = MockEnvironment::new(INVALID_FUNC_ID, input.clone()); + let mut extension = Extension::::default(); + assert!(matches!( + extension.call(&mut env), + Err(error) if error == pallet_contracts::Error::::DecodingFailed.into() + )); + // Charges weight. + assert_eq!(env.charged(), overhead_weight(input.len() as u32)) + } + + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + let encoded_call = call.encode(); + let mut env = MockEnvironment::new(DispatchExtFuncId::get(), encoded_call.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(encoded_call.len() as u32) + + read_from_buffer_weight(encoded_call.len() as u32) + + call.get_dispatch_info().weight + ); + }); + } + + #[test] + fn dispatch_call_with_invalid_input_returns_error() { + // Invalid encoded runtime call. + let input = vec![0u8, 99]; + let mut env = MockEnvironment::new(DispatchExtFuncId::get(), input.clone()); + let mut extension = Extension::::default(); + assert!(extension.call(&mut env).is_err()); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(input.len() as u32) + read_from_buffer_weight(input.len() as u32) + ); + } + + #[test] + fn read_state_works() { + let read = RuntimeRead::Ping; + let encoded_read = read.encode(); + let expected = "pop".as_bytes().encode(); + let mut env = MockEnvironment::new(ReadExtFuncId::get(), encoded_read.clone()); + let mut extension = Extension::::default(); + assert!(matches!(extension.call(&mut env), Ok(Converging(0)))); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(encoded_read.len() as u32) + + read_from_buffer_weight(encoded_read.len() as u32) + + read.weight() + + write_to_contract_weight(expected.len() as u32) + ); + // Check if the contract environment buffer is written correctly. + assert_eq!(env.buffer, expected); + } + + #[test] + fn read_state_with_invalid_input_returns_error() { + let input = vec![0u8, 99]; + let mut env = MockEnvironment::new( + ReadExtFuncId::get(), + // Invalid runtime state read. + input.clone(), + ); + let mut extension = Extension::::default(); + assert!(extension.call(&mut env).is_err()); + // Charges weight. + assert_eq!( + env.charged(), + overhead_weight(input.len() as u32) + read_from_buffer_weight(input.len() as u32) + ); + } + + // Weight charged for calling into the runtime from a contract. + fn overhead_weight(input_len: u32) -> Weight { + ContractWeightsOf::::seal_debug_message(input_len) + } + + // Weight charged for reading function call input from buffer. + pub(crate) fn read_from_buffer_weight(input_len: u32) -> Weight { + ContractWeightsOf::::seal_return(input_len) + } + + // Weight charged for writing to contract memory. + pub(crate) fn write_to_contract_weight(len: u32) -> Weight { + ContractWeightsOf::::seal_input(len) + } +} diff --git a/extension/src/matching.rs b/extension/src/matching.rs new file mode 100644 index 00000000..94939597 --- /dev/null +++ b/extension/src/matching.rs @@ -0,0 +1,91 @@ +use super::*; + +/// Trait for matching a function. +pub trait Matches { + /// Determines whether a function is a match. + /// + /// # Parameters + /// - `env` - The current execution environment. + fn matches(env: &impl Environment) -> bool; +} + +/// Matches an extension and function identifier. +pub struct Equals(PhantomData<(E, F)>); +impl, FuncId: Get> Matches for Equals { + fn matches(env: &impl Environment) -> bool { + env.ext_id() == ExtId::get() && env.func_id() == FuncId::get() + } +} + +/// Matches a function identifier only. +pub struct FunctionId(PhantomData); +impl> Matches for FunctionId { + fn matches(env: &impl Environment) -> bool { + env.func_id() == T::get() + } +} + +/// Matches a `u32` function identifier. +pub struct WithFuncId(PhantomData); +impl> Matches for WithFuncId { + fn matches(env: &impl Environment) -> bool { + let ext_id: [u8; 2] = env.ext_id().to_le_bytes(); + let func_id: [u8; 2] = env.func_id().to_le_bytes(); + u32::from_le_bytes([func_id[0], func_id[1], ext_id[0], ext_id[1]]) == T::get() + } +} + +#[cfg(test)] +mod tests { + use sp_core::{ConstU16, ConstU32}; + + use super::*; + use crate::mock::MockEnvironment; + + #[test] + fn equals_matches() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 2]), vec![]); + assert!(Equals::, ConstU16<2>>::matches(&env)); + } + + #[test] + fn equals_does_not_match() { + // Fails due to the invalid function id. + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 3]), vec![]); + assert!(!Equals::, ConstU16<2>>::matches(&env)); + + // Fails due to the invalid extension id. + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 2, 0, 2]), vec![]); + assert!(!Equals::, ConstU16<2>>::matches(&env)); + } + + #[test] + fn function_id_matches() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 2]), vec![]); + assert!(FunctionId::>::matches(&env)); + } + + #[test] + fn function_id_does_not_match() { + let env = MockEnvironment::new(u32::from_be_bytes([0u8, 1, 0, 3]), vec![]); + assert!(!FunctionId::>::matches(&env)); + } + + #[test] + fn with_func_id_matches() { + let env = MockEnvironment::default(); + assert!(WithFuncId::>::matches(&env)); + + let env = MockEnvironment::new(1, vec![]); + assert!(WithFuncId::>::matches(&env)); + + let env = MockEnvironment::new(100, vec![]); + assert!(WithFuncId::>::matches(&env)); + } + + #[test] + fn with_func_id_does_not_match() { + let env = MockEnvironment::new(1, vec![]); + assert!(!WithFuncId::>::matches(&env)); + } +} diff --git a/extension/src/mock.rs b/extension/src/mock.rs new file mode 100644 index 00000000..c085aebe --- /dev/null +++ b/extension/src/mock.rs @@ -0,0 +1,390 @@ +use std::marker::PhantomData; + +use codec::{Decode, Encode}; +use frame_support::{ + derive_impl, + pallet_prelude::Weight, + parameter_types, + traits::{fungible::Inspect, ConstU32, Everything, Nothing}, +}; +use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; +use pallet_contracts::{chain_extension::RetVal, DefaultAddressGenerator, Frame, Schedule}; +use sp_runtime::{BuildStorage, DispatchError, Perbill}; + +use crate::{ + decoding::Identity, environment, matching::WithFuncId, AccountIdOf, ContractWeightsOf, + Converter, Decodes, DecodingFailed, DefaultConverter, DispatchCall, Extension, Function, + Matches, Processor, ReadState, Readable, +}; + +pub(crate) const ALICE: u64 = 1; +pub(crate) const DEBUG_OUTPUT: pallet_contracts::DebugInfo = + pallet_contracts::DebugInfo::UnsafeDebug; +pub(crate) const GAS_LIMIT: Weight = Weight::from_parts(500_000_000_000, 3 * 1024 * 1024); +pub(crate) const INIT_AMOUNT: ::Balance = 100_000_000; +pub(crate) const INVALID_FUNC_ID: u32 = 0; + +pub(crate) type AccountId = AccountIdOf; +pub(crate) type Balance = + <::Currency as Inspect>>::Balance; +type DispatchCallWith>> = DispatchCall< + // Registered with func id 1 + WithFuncId, + // Runtime config + Test, + // Decode inputs to the function as runtime calls + Decodes, DecodingFailed, Processor>, + // Accept any filtering + Filter, +>; +pub(crate) type EventRecord = + frame_system::EventRecord<::RuntimeEvent, HashOf>; +type HashOf = ::Hash; +pub(crate) type MockEnvironment = Environment; +type ReadStateWith>> = ReadState< + // Registered with func id 1 + WithFuncId, + // Runtime config + Test, + // The runtime state reads available. + RuntimeRead, + // Decode inputs to the function as runtime calls + Decodes, DecodingFailed, Processor>, + // Accept any filtering + Filter, + // Convert the result of a read into the expected result + DefaultConverter, +>; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Contracts: pallet_contracts, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type Block = frame_system::mocking::MockBlock; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; + type ReserveIdentifier = [u8; 8]; +} + +#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig as pallet_timestamp::DefaultConfig)] +impl pallet_timestamp::Config for Test {} + +impl pallet_contracts::Config for Test { + type AddressGenerator = DefaultAddressGenerator; + type ApiVersion = (); + type CallFilter = (); + // TestFilter; + type CallStack = [Frame; 5]; + type ChainExtension = Extension; + type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; + type Currency = Balances; + type Debug = (); + // TestDebug; + type DefaultDepositLimit = DefaultDepositLimit; + type DepositPerByte = DepositPerByte; + type DepositPerItem = DepositPerItem; + type Environment = (); + type InstantiateOrigin = EnsureSigned; + type MaxCodeLen = ConstU32<{ 100 * 1024 }>; + type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; + type MaxDelegateDependencies = MaxDelegateDependencies; + type MaxStorageKeyLen = ConstU32<128>; + type Migrations = (); + // crate::migration::codegen::BenchMigrations; + type Randomness = Test; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type Schedule = MySchedule; + type Time = Timestamp; + type UnsafeUnstableInterface = (); + // UnstableInterface; + type UploadOrigin = EnsureSigned; + type WeightInfo = (); + type WeightPrice = (); + // Self; + type Xcm = (); +} + +parameter_types! { + pub MySchedule: Schedule = { + let schedule = >::default(); + schedule + }; + pub static DepositPerByte: ::Balance = 1; + pub const DepositPerItem: ::Balance = 2; + pub static MaxDelegateDependencies: u32 = 32; + pub static MaxTransientStorageSize: u32 = 4 * 1024; + pub static CodeHashLockupDepositPercent: Perbill = Perbill::from_percent(0); + pub static DefaultDepositLimit: ::Balance = 10_000_000; +} + +impl frame_support::traits::Randomness, BlockNumberFor> for Test { + fn random(_subject: &[u8]) -> (HashOf, BlockNumberFor) { + (Default::default(), Default::default()) + } +} + +parameter_types! { + // IDs for functions for extension tests. + pub const DispatchExtFuncId : u32 = 1; + pub const ReadExtFuncId : u32 = 2; + // IDs for functions for contract tests. + pub const DispatchContractFuncId : u32 = 3; + pub const ReadContractFuncId : u32 = 4; + // IDs for function for contract tests but do nothing. + pub const DispatchContractNoopFuncId : u32 = 5; + pub const ReadContractNoopFuncId : u32 = 6; + // ID for function that does nothing + pub const NoopFuncId : u32 = u32::MAX; +} + +/// A query of mock runtime state. +#[derive(Encode, Decode, Debug)] +#[repr(u8)] +pub enum RuntimeRead { + #[codec(index = 1)] + Ping, +} +impl Readable for RuntimeRead { + /// The corresponding type carrying the result of the query for mock runtime state. + type Result = RuntimeResult; + + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. + fn weight(&self) -> Weight { + match self { + RuntimeRead::Ping => Weight::from_parts(1_000u64, 1u64), + } + } + + /// Performs the read and returns the result. + fn read(self) -> Self::Result { + match self { + RuntimeRead::Ping => RuntimeResult::Pong("pop".to_string()), + } + } +} + +/// The result of a mock runtime state read. +#[derive(Debug, Decode, Encode)] +pub enum RuntimeResult { + #[codec(index = 1)] + Pong(String), +} + +impl Into> for RuntimeResult { + fn into(self) -> Vec { + match self { + RuntimeResult::Pong(value) => value.encode(), + } + } +} + +pub(crate) type Functions = ( + // Functions that allow everything for extension testing. + DispatchCallWith, + ReadStateWith, + // Functions that allow everything for contract testing. + DispatchCallWith, + ReadStateWith, + // Functions that allow nothing for contract testing. + DispatchCallWith, + ReadStateWith, + // Function that does nothing. + Noop, Test>, +); + +#[derive(Default)] +pub struct Config; +impl super::Config for Config { + type Functions = Functions; + + const LOG_TARGET: &'static str = "pop-chain-extension"; +} + +// Removes first bytes of the encoded call, added by the chain extension call within the proxy +// contract. +pub struct RemoveFirstByte; +impl Processor for RemoveFirstByte { + type Value = Vec; + + const LOG_TARGET: &'static str = ""; + + fn process(mut value: Self::Value, _env: &impl crate::Environment) -> Self::Value { + if !value.is_empty() { + value.remove(0); + } + value + } +} + +// A function that does nothing. +pub struct Noop(PhantomData<(M, C)>); +impl Function for Noop { + type Config = Config; + type Error = (); + + fn execute( + _env: &mut (impl environment::Environment + crate::BufIn), + ) -> pallet_contracts::chain_extension::Result { + Ok(RetVal::Converging(0)) + } +} +impl Matches for Noop { + fn matches(env: &impl crate::Environment) -> bool { + M::matches(env) + } +} + +/// A mocked chain extension environment. +pub(crate) struct Environment { + func_id: u16, + ext_id: u16, + charged: Vec, + pub(crate) buffer: Vec, + ext: E, +} + +impl Default for Environment { + fn default() -> Self { + Self::new(0, [].to_vec()) + } +} + +impl Environment { + pub(crate) fn new(id: u32, buffer: Vec) -> Self { + Self { + func_id: (id & 0x0000FFFF) as u16, + ext_id: (id >> 16) as u16, + charged: Vec::new(), + buffer, + ext: E::default(), + } + } + + pub(crate) fn charged(&self) -> Weight { + self.charged.iter().fold(Weight::zero(), |acc, b| acc.saturating_add(*b)) + } +} + +impl> + Clone> environment::Environment + for Environment +{ + type AccountId = E::AccountId; + type ChargedAmount = Weight; + + fn func_id(&self) -> u16 { + self.func_id + } + + fn ext_id(&self) -> u16 { + self.ext_id + } + + fn charge_weight( + &mut self, + amount: Weight, + ) -> pallet_contracts::chain_extension::Result { + self.charged.push(amount); + Ok(amount) + } + + fn adjust_weight(&mut self, charged: Self::ChargedAmount, actual_weight: Weight) { + let last = self + .charged + .iter() + .enumerate() + .filter_map(|(i, c)| (c == &charged).then_some(i)) + .last() + .unwrap(); + self.charged.remove(last); + self.charged.insert(last, actual_weight) + } + + fn ext(&mut self) -> impl environment::Ext { + self.ext.clone() + } +} + +impl environment::BufIn for Environment { + fn in_len(&self) -> u32 { + self.buffer.len() as u32 + } + + fn read(&self, _max_len: u32) -> pallet_contracts::chain_extension::Result> { + // TODO: handle max_len + Ok(self.buffer.clone()) + } +} + +impl environment::BufOut for Environment { + fn write( + &mut self, + buffer: &[u8], + _allow_skip: bool, + _weight_per_byte: Option, + ) -> pallet_contracts::chain_extension::Result<()> { + self.buffer = buffer.to_vec(); + Ok(()) + } +} + +/// A mocked smart contract environment. +#[derive(Clone, Default)] +pub(crate) struct MockExt { + pub(crate) address: AccountIdOf, +} +impl environment::Ext for MockExt { + type AccountId = AccountIdOf; + + fn address(&self) -> &Self::AccountId { + &self.address + } +} + +/// Test externalities. +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances: vec![(ALICE, INIT_AMOUNT)] } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +/// A converter for converting string results to uppercase. +pub(crate) struct UppercaseConverter; +impl Converter for UppercaseConverter { + type Error = DispatchError; + type Source = RuntimeResult; + type Target = Vec; + + const LOG_TARGET: &'static str = ""; + + fn try_convert( + value: Self::Source, + _env: &impl crate::Environment, + ) -> Result { + match value { + RuntimeResult::Pong(value) => Ok(value.to_uppercase().encode()), + } + } +} diff --git a/extension/src/tests.rs b/extension/src/tests.rs new file mode 100644 index 00000000..c9588cd3 --- /dev/null +++ b/extension/src/tests.rs @@ -0,0 +1,231 @@ +use core::fmt::Debug; +use std::{path::Path, sync::LazyLock}; + +use codec::{Decode, Encode}; +use frame_support::weights::Weight; +use frame_system::Call; +use pallet_contracts::{Code, CollectEvents, ContractExecResult, Determinism, StorageDeposit}; +use sp_runtime::{ + DispatchError::{self, BadOrigin, Module}, + ModuleError, +}; + +use crate::{ + mock::{self, *}, + ErrorConverter, +}; + +static CONTRACT: LazyLock> = + LazyLock::new(|| initialize_contract("contract/target/ink/proxy.wasm")); + +mod dispatch_call { + use super::*; + + #[test] + fn dispatch_call_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractFuncId::get(), + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + // Successfully return data. + let return_value = dispatch_result.result.unwrap(); + let decoded = , u32>>::decode(&mut &return_value.data[..]).unwrap(); + assert!(decoded.unwrap().is_empty()); + // Successfully emit event. + assert!(dispatch_result.events.unwrap().iter().any(|e| matches!(e.event, + RuntimeEvent::System(frame_system::Event::::Remarked { sender, .. }) + if sender == contract))); + assert_eq!(dispatch_result.storage_deposit, StorageDeposit::Charge(0)); + }); + } + + #[test] + fn dispatch_call_filtering_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractNoopFuncId::get(), + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + assert_eq!( + dispatch_result.result, + Err(Module(ModuleError { + index: 0, + error: [5, 0, 0, 0], + message: Some("CallFiltered") + })) + ); + }); + } + + #[test] + fn dispatch_call_returns_error() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let dispatch_result = call( + contract, + DispatchContractFuncId::get(), + // `set_code` requires root origin, expect throwing error. + RuntimeCall::System(Call::set_code { code: "pop".as_bytes().to_vec() }), + GAS_LIMIT, + ); + assert_eq!(dispatch_result.result.err(), Some(BadOrigin)) + }) + } +} + +mod read_state { + use super::*; + + #[test] + fn read_state_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + // Successfully return data. + let read_result = + call(contract, ReadContractFuncId::get(), RuntimeRead::Ping, GAS_LIMIT); + let return_value = read_result.result.unwrap(); + let decoded = , u32>>::decode(&mut &return_value.data[1..]).unwrap(); + let result = Ok("pop".as_bytes().to_vec()); + assert_eq!(decoded, result); + }); + } + + #[test] + fn read_state_filtering_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + // Successfully return data. + let read_result = + call(contract, ReadContractNoopFuncId::get(), RuntimeRead::Ping, GAS_LIMIT); + assert_eq!( + read_result.result, + Err(Module(ModuleError { + index: 0, + error: [5, 0, 0, 0], + message: Some("CallFiltered") + })) + ); + }); + } + + #[test] + fn read_state_with_invalid_input_returns_error() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let read_result = call(contract, ReadExtFuncId::get(), 99u8, GAS_LIMIT); + let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); + // Make sure the error is passed through the error converter. + let error = + <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); + assert_eq!(read_result.result.err(), error); + }) + } +} + +#[test] +fn noop_function_works() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let noop_result = call(contract, NoopFuncId::get(), (), GAS_LIMIT); + // Successfully return data. + let return_value = noop_result.result.unwrap(); + let decoded = , u32>>::decode(&mut &return_value.data[..]).unwrap(); + assert!(decoded.unwrap().is_empty()); + assert_eq!(noop_result.storage_deposit, StorageDeposit::Charge(0)); + }) +} + +#[test] +fn invalid_func_id_fails() { + new_test_ext().execute_with(|| { + // Instantiate a new contract. + let contract = instantiate(); + let result = call(contract, INVALID_FUNC_ID, (), GAS_LIMIT); + let expected: DispatchError = pallet_contracts::Error::::DecodingFailed.into(); + // Make sure the error is passed through the error converter. + let error = + <() as ErrorConverter>::convert(expected, &mock::MockEnvironment::default()).err(); + assert_eq!(result.result.err(), error); + }); +} + +/// Initializing a new contract file if it does not exist. +fn initialize_contract(contract_path: &str) -> Vec { + if !Path::new(contract_path).exists() { + use contract_build::*; + let manifest_path = ManifestPath::new("contract/Cargo.toml").unwrap(); + let args = ExecuteArgs { + build_artifact: BuildArtifacts::CodeOnly, + build_mode: BuildMode::Debug, + manifest_path, + output_type: OutputType::Json, + verbosity: Verbosity::Quiet, + skip_wasm_validation: true, + ..Default::default() + }; + execute(args).unwrap(); + } + std::fs::read(contract_path).unwrap() +} + +/// Instantiating the contract. +fn instantiate() -> AccountId { + let result = Contracts::bare_instantiate( + ALICE, + 0, + GAS_LIMIT, + None, + Code::Upload(CONTRACT.clone()), + function_selector("new"), + Default::default(), + DEBUG_OUTPUT, + CollectEvents::UnsafeCollect, + ); + log::debug!("instantiate result: {result:?}"); + let result = result.result.unwrap(); + assert!(!result.result.did_revert()); + result.account_id +} + +/// Perform a call to a specified contract. +fn call( + contract: AccountId, + func_id: u32, + input: impl Encode + Debug, + gas_limit: Weight, +) -> ContractExecResult { + log::debug!("call: func_id={func_id}, input={input:?}"); + let result = Contracts::bare_call( + ALICE, + contract, + 0, + gas_limit, + None, + [function_selector("call"), (func_id, input.encode()).encode()].concat(), + DEBUG_OUTPUT, + CollectEvents::UnsafeCollect, + Determinism::Enforced, + ); + log::debug!("gas consumed: {:?}", result.gas_consumed); + log::debug!("call result: {result:?}"); + result +} + +/// Construct the hashed bytes as a selector of function. +fn function_selector(name: &str) -> Vec { + sp_io::hashing::blake2_256(name.as_bytes())[0..4].to_vec() +} diff --git a/pallets/api/Cargo.toml b/pallets/api/Cargo.toml new file mode 100644 index 00000000..55a00789 --- /dev/null +++ b/pallets/api/Cargo.toml @@ -0,0 +1,61 @@ +[package] +authors.workspace = true +description = "API pallet, enabling smart(er) contracts with the power of Polkadot" +edition.workspace = true +license.workspace = true +name = "pallet-api" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +codec.workspace = true +log.workspace = true +scale-info.workspace = true + +# Local +pop-chain-extension.workspace = true + +# Substrate +frame-benchmarking.workspace = true +frame-support.workspace = true +frame-system.workspace = true +pallet-assets.workspace = true +sp-runtime.workspace = true +sp-std.workspace = true + +[dev-dependencies] +pallet-balances.workspace = true +sp-core.workspace = true +sp-io.workspace = true + +[features] +default = [ "std" ] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pop-chain-extension/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "pallet-assets/std", + "pallet-balances/std", + "pop-chain-extension/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/api/src/extension.rs b/pallets/api/src/extension.rs new file mode 100644 index 00000000..0fd22f2c --- /dev/null +++ b/pallets/api/src/extension.rs @@ -0,0 +1,360 @@ +use core::{fmt::Debug, marker::PhantomData}; + +use frame_support::traits::Get; +pub use pop_chain_extension::{ + Config, ContractWeightsOf, DecodingFailed, DispatchCall, ErrorConverter, ReadState, Readable, +}; +use pop_chain_extension::{ + Converter, Decodes, Environment, LogTarget, Matches, Processor, Result, RetVal, +}; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; + +/// Encoded version of `pallet_contracts::Error::DecodingFailed`, as found within +/// `DispatchError::ModuleError`. +pub const DECODING_FAILED_ERROR: [u8; 4] = [11, 0, 0, 0]; +/// The logging target for the chain extension. +pub const LOG_TARGET: &str = "pop-api::extension"; + +/// The chain extension used by the API. +pub type Extension = pop_chain_extension::Extension; +/// Decodes output by prepending bytes from ext_id() + func_id() +pub type DecodesAs = + Decodes; + +/// Prepends bytes from ext_id() + func_id() to prefix the encoded input bytes to determine the +/// versioned output +pub struct Prepender; +impl Processor for Prepender { + /// The type of value to be processed. + type Value = Vec; + + /// The log target. + const LOG_TARGET: &'static str = "pop-api::extension::processor"; + + /// Processes the provided value. + /// + /// # Parameters + /// - `value` - The value to be processed. + /// - `env` - The current execution environment. + fn process(mut value: Self::Value, env: &impl Environment) -> Self::Value { + // Resolve version, pallet and call index from environment + let version = version(env); + let (module, index) = module_and_index(env); + // Prepend bytes + value.splice(0..0, [version, module, index]); + log::debug!(target: Self::LOG_TARGET, "prepender: version={version}, module={module}, index={index}"); + value + } +} + +/// Matches on the first byte of a function identifier only. +pub struct IdentifiedByFirstByteOfFunctionId(PhantomData); +impl> Matches for IdentifiedByFirstByteOfFunctionId { + fn matches(env: &impl Environment) -> bool { + func_id(env) == T::get() + } +} + +/// A log target for dispatched calls. +pub struct DispatchCallLogTarget; +impl LogTarget for DispatchCallLogTarget { + const LOG_TARGET: &'static str = "pop-api::extension::dispatch"; +} + +/// A log target for state reads. +pub struct ReadStateLogTarget; +impl LogTarget for ReadStateLogTarget { + const LOG_TARGET: &'static str = "pop-api::extension::read-state"; +} + +/// Conversion of a `DispatchError` to a versioned error. +pub struct VersionedErrorConverter(PhantomData); +impl + Into + Debug> ErrorConverter + for VersionedErrorConverter +{ + /// The log target. + const LOG_TARGET: &'static str = "pop-api::extension::converters::versioned-error"; + + /// Converts the provided error. + /// + /// # Parameters + /// - `error` - The error to be converted. + /// - `env` - The current execution environment. + fn convert(error: DispatchError, env: &impl Environment) -> Result { + // Defer to supplied versioned error conversion type + let version = version(env); + log::debug!(target: Self::LOG_TARGET, "versioned error converter: error={error:?}, version={version}"); + let error: Error = (error, version).try_into()?; + log::debug!(target: Self::LOG_TARGET, "versioned error converter: converted error={error:?}"); + Ok(RetVal::Converging(error.into())) + } +} + +/// Conversion of a read result to a versioned read result. +pub struct VersionedResultConverter(PhantomData<(S, T)>); +impl + Debug> Converter + for VersionedResultConverter +{ + /// The type returned in the event of a conversion error. + type Error = DispatchError; + /// The type of value to be converted. + type Source = Source; + /// The target type. + type Target = Target; + + /// The log target. + const LOG_TARGET: &'static str = "pop-api::extension::converters::versioned-result"; + + /// Converts the provided value. + /// + /// # Parameters + /// - `value` - The value to be converted. + /// - `env` - The current execution environment. + fn try_convert(value: Self::Source, env: &impl Environment) -> Result { + // Defer to supplied versioned result conversion type. + let version = version(env); + log::debug!(target: Self::LOG_TARGET, "versioned result converter: result={value:?}, version={version}"); + let converted: Target = (value, version).try_into()?; + log::debug!(target: Self::LOG_TARGET, "versioned result converter: converted result={converted:?}"); + Ok(converted) + } +} + +fn func_id(env: &impl Environment) -> u8 { + env.func_id().to_le_bytes()[0] +} + +fn module_and_index(env: &impl Environment) -> (u8, u8) { + let bytes = env.ext_id().to_le_bytes(); + (bytes[0], bytes[1]) +} + +fn version(env: &impl Environment) -> u8 { + env.func_id().to_le_bytes()[1] +} + +#[cfg(test)] +mod tests { + use frame_support::pallet_prelude::Weight; + use pop_chain_extension::Ext; + use sp_core::ConstU8; + + use super::{DispatchError::*, *}; + use crate::extension::Prepender; + + #[test] + fn func_id_works() { + let env = MockEnvironment { func_id: u16::from_le_bytes([1, 2]), ext_id: 0u16 }; + assert_eq!(func_id(&env), 1); + } + + #[test] + fn module_and_index_works() { + let env = MockEnvironment { func_id: 0u16, ext_id: u16::from_le_bytes([2, 3]) }; + assert_eq!(module_and_index(&env), (2, 3)); + } + + #[test] + fn version_works() { + let env = MockEnvironment { func_id: u16::from_le_bytes([1, 2]), ext_id: 0u16 }; + assert_eq!(version(&env), 2); + } + + #[test] + fn prepender_works() { + let func = 0; + let version = 1; + let module = 2; + let index = 3; + let env = MockEnvironment { + func_id: u16::from_le_bytes([func, version]), + ext_id: u16::from_le_bytes([module, index]), + }; + let data = 42; + assert_eq!(Prepender::process(vec![data], &env), vec![version, module, index, data]); + } + + #[test] + fn identified_by_first_byte_of_function_id_matches() { + let env = MockEnvironment { func_id: u16::from_le_bytes([1, 2]), ext_id: 0u16 }; + assert!(IdentifiedByFirstByteOfFunctionId::>::matches(&env)); + } + + #[test] + fn identified_by_first_byte_of_function_id_does_not_match() { + let env = MockEnvironment { func_id: u16::from_le_bytes([1, 2]), ext_id: 0u16 }; + assert!(!IdentifiedByFirstByteOfFunctionId::>::matches(&env)); + } + + #[test] + fn dispatch_call_log_target_works() { + assert!(matches!( + ::LOG_TARGET, + "pop-api::extension::dispatch" + )); + } + + #[test] + fn read_state_log_target_works() { + assert!(matches!( + ::LOG_TARGET, + "pop-api::extension::read-state" + )); + } + + mod versioned_error { + use super::{RetVal::Converging, *}; + + // Mock versioned error. + #[derive(Debug)] + pub enum VersionedError { + V0(DispatchError), + V1(DispatchError), + } + + impl TryFrom<(DispatchError, u8)> for VersionedError { + type Error = DispatchError; + + fn try_from(value: (DispatchError, u8)) -> Result { + let (error, version) = value; + match version { + 0 => Ok(VersionedError::V0(error)), + 1 => Ok(VersionedError::V1(error)), + _ => Err(Other("DecodingFailed")), + } + } + } + + impl From for u32 { + // Mock conversion based on error and version. + fn from(value: VersionedError) -> Self { + match value { + VersionedError::V0(error) => match error { + BadOrigin => 1, + _ => 100, + }, + VersionedError::V1(error) => match error { + BadOrigin => 2, + _ => 200, + }, + } + } + } + + #[test] + fn versioned_error_converter_works() { + for (version, error, expected_result) in vec![ + (0, BadOrigin, 1), + (0, Other("test"), 100), + (1, BadOrigin, 2), + (1, Other("test"), 200), + ] { + let env = + MockEnvironment { func_id: u16::from_le_bytes([0, version]), ext_id: 0u16 }; + // Because `Retval` does not implement the `Debug` trait the result has to be + // unwrapped. + let Ok(Converging(result)) = + VersionedErrorConverter::::convert(error, &env) + else { + panic!("should not happen") + }; + assert_eq!(result, expected_result); + } + } + + #[test] + fn versioned_error_converter_fails_when_invalid_version() { + let version = 2; + let env = MockEnvironment { func_id: u16::from_le_bytes([0, version]), ext_id: 0u16 }; + let result = VersionedErrorConverter::::convert(BadOrigin, &env).err(); + assert_eq!(result, Some(Other("DecodingFailed"))); + } + } + + mod versioned_result { + use VersionedRuntimeResult::{V0, V1}; + + use super::*; + + // Mock versioned runtime result. + #[derive(Debug, PartialEq)] + pub enum VersionedRuntimeResult { + V0(u8), + V1(u8), + } + + impl TryFrom<(u8, u8)> for VersionedRuntimeResult { + type Error = DispatchError; + + // Mock conversion based on result and version. + fn try_from(value: (u8, u8)) -> Result { + let (result, version) = value; + // Per version there is a specific upper bound allowed. + match version { + 0 if result <= 50 => Ok(V0(result)), + 0 if result > 50 => Ok(V0(50)), + 1 if result <= 100 => Ok(V1(result)), + 1 if result > 100 => Ok(V1(100)), + _ => Err(Other("DecodingFailed")), + } + } + } + + #[test] + fn versioned_result_converter_works() { + for (version, value, expected_result) in vec![ + (0, 10, Ok(V0(10))), + // `V0` has 50 as upper bound and therefore caps the value. + (0, 100, Ok(V0(50))), + (1, 10, Ok(V1(10))), + // Different upper bound for `V1`. + (1, 100, Ok(V1(100))), + ] { + let env = + MockEnvironment { func_id: u16::from_le_bytes([0, version]), ext_id: 0u16 }; + let result = VersionedResultConverter::::try_convert( + value, &env, + ); + assert_eq!(result, expected_result); + } + } + + #[test] + fn versioned_result_converter_fails_when_invalid_version() { + let env = MockEnvironment { func_id: u16::from_le_bytes([0, 2]), ext_id: 0u16 }; + let result = + VersionedResultConverter::::try_convert(10, &env).err(); + assert_eq!(result, Some(Other("DecodingFailed"))); + } + } + + struct MockEnvironment { + func_id: u16, + ext_id: u16, + } + impl Environment for MockEnvironment { + type AccountId = (); + type ChargedAmount = Weight; + + fn func_id(&self) -> u16 { + self.func_id + } + + fn ext_id(&self) -> u16 { + self.ext_id + } + + fn charge_weight(&mut self, _amount: Weight) -> Result { + unimplemented!() + } + + fn adjust_weight(&mut self, _charged: Self::ChargedAmount, _actual_weight: Weight) { + unimplemented!() + } + + fn ext(&mut self) -> impl Ext { + unimplemented!() + } + } +} diff --git a/pallets/api/src/fungibles/benchmarking.rs b/pallets/api/src/fungibles/benchmarking.rs new file mode 100644 index 00000000..7ded38c0 --- /dev/null +++ b/pallets/api/src/fungibles/benchmarking.rs @@ -0,0 +1,178 @@ +//! Benchmarking setup for pallet_api::fungibles + +use frame_benchmarking::{account, v2::*}; +use frame_support::{ + assert_ok, + traits::{ + fungibles::{ + approvals::{Inspect as ApprovalInspect, Mutate}, + Create, Inspect, + }, + Currency, + }, +}; +use frame_system::RawOrigin; +use sp_runtime::traits::Zero; + +use super::{ + AccountIdOf, AssetsInstanceOf, AssetsOf, BalanceOf, Call, Config, Pallet, Read, TokenIdOf, +}; +use crate::Read as _; + +const SEED: u32 = 1; + +// See if `generic_event` has been emitted. +fn assert_has_event( + generic_event: >>::RuntimeEvent, +) { + frame_system::Pallet::::assert_has_event(generic_event.into()); +} + +#[benchmarks( + where + > as Inspect<::AccountId>>::AssetId: Zero, +)] +mod benchmarks { + use super::*; + + // Parameter: + // - 'a': whether `approve_transfer` is required. + // - 'c': whether `cancel_approval` is required. + #[benchmark] + fn approve(a: Linear<0, 1>, c: Linear<0, 1>) -> Result<(), BenchmarkError> { + let token_id = TokenIdOf::::zero(); + let min_balance = >::from(1u32); + let owner: AccountIdOf = account("Alice", 0, SEED); + let spender: AccountIdOf = account("Bob", 0, SEED); + let current_allowance = >::from(u32::MAX / 2); + T::Currency::make_free_balance_be(&owner, u32::MAX.into()); + // Set the `current_allowance`. + assert_ok!( as Create>>::create( + token_id.clone(), + owner.clone(), + true, + min_balance + )); + assert_ok!( as Mutate>>::approve( + token_id.clone(), + &owner, + &spender, + current_allowance, + )); + let approval_value = match (a, c) { + // Equal to the current allowance. + (0, 0) => current_allowance, + // Greater than the current allowance. + (1, 0) => >::from(u32::MAX), + // Zero. + (0, 1) => >::from(0u32), + // Smaller than the current allowance. + (1, 1) => >::from(u32::MAX / 4), + _ => unreachable!("values can only be 0 or 1"), + }; + + #[extrinsic_call] + _(RawOrigin::Signed(owner.clone()), token_id.clone(), spender.clone(), approval_value); + + assert_eq!(AssetsOf::::allowance(token_id.clone(), &owner, &spender), approval_value); + if c == 1 { + assert_has_event::( + pallet_assets::Event::ApprovalCancelled { + asset_id: token_id.clone(), + owner: owner.clone(), + delegate: spender.clone(), + } + .into(), + ); + } + if a == 1 { + let amount = match c { + // When the allowance was cancelled and then approved with the new value. + 1 => approval_value, + // When the allowance was increased. + 0 => approval_value - current_allowance, + _ => unreachable!("`c` can only be 0 or 1"), + }; + assert_has_event::( + pallet_assets::Event::ApprovedTransfer { + asset_id: token_id, + source: owner, + delegate: spender, + amount, + } + .into(), + ); + } + Ok(()) + } + + #[benchmark] + // Storage: `Assets` + fn total_supply() { + #[block] + { + Pallet::::read(Read::TotalSupply(TokenIdOf::::zero())); + } + } + #[benchmark] + // Storage: `AssetAccount` + fn balance_of() { + #[block] + { + Pallet::::read(Read::BalanceOf { + token: TokenIdOf::::zero(), + owner: account("Alice", 0, SEED), + }); + } + } + #[benchmark] + // Storage: `Approval` + fn allowance() { + #[block] + { + Pallet::::read(Read::Allowance { + token: TokenIdOf::::zero(), + owner: account("Alice", 0, SEED), + spender: account("Bob", 0, SEED), + }); + } + } + + #[benchmark] + // Storage: `AssetMetadata` + fn token_name() { + #[block] + { + Pallet::::read(Read::TokenName(TokenIdOf::::zero())); + } + } + + #[benchmark] + // Storage: `AssetMetadata` + fn token_symbol() { + #[block] + { + Pallet::::read(Read::TokenSymbol(TokenIdOf::::zero())); + } + } + + #[benchmark] + // Storage: `AssetMetadata` + fn token_decimals() { + #[block] + { + Pallet::::read(Read::TokenDecimals(TokenIdOf::::zero())); + } + } + + #[benchmark] + // Storage: `Assets` + fn token_exists() { + #[block] + { + Pallet::::read(Read::TokenExists(TokenIdOf::::zero())); + } + } + + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/api/src/fungibles/mod.rs b/pallets/api/src/fungibles/mod.rs new file mode 100644 index 00000000..4c840628 --- /dev/null +++ b/pallets/api/src/fungibles/mod.rs @@ -0,0 +1,530 @@ +//! The fungibles pallet offers a streamlined interface for interacting with fungible tokens. The +//! goal is to provide a simplified, consistent API that adheres to standards in the smart contract +//! space. + +use frame_support::traits::fungibles::{metadata::Inspect as MetadataInspect, Inspect}; +pub use pallet::*; +use pallet_assets::WeightInfo as AssetsWeightInfoTrait; +use weights::WeightInfo; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +#[cfg(test)] +mod tests; +pub mod weights; + +type AccountIdOf = ::AccountId; +type TokenIdOf = as Inspect<::AccountId>>::AssetId; +type TokenIdParameterOf = >>::AssetIdParameter; +type AssetsOf = pallet_assets::Pallet>; +type AssetsInstanceOf = ::AssetsInstance; +type AssetsWeightInfoOf = >>::WeightInfo; +type BalanceOf = as Inspect<::AccountId>>::Balance; +type WeightOf = ::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use core::cmp::Ordering::*; + + use frame_support::{ + dispatch::{DispatchResult, DispatchResultWithPostInfo, WithPostDispatchInfo}, + pallet_prelude::*, + traits::fungibles::approvals::Inspect as ApprovalInspect, + }; + use frame_system::pallet_prelude::*; + use sp_runtime::{ + traits::{StaticLookup, Zero}, + Saturating, + }; + use sp_std::vec::Vec; + + use super::*; + + /// State reads for the fungibles API with required input. + #[derive(Encode, Decode, Debug, MaxEncodedLen)] + #[cfg_attr(feature = "std", derive(PartialEq, Clone))] + #[repr(u8)] + #[allow(clippy::unnecessary_cast)] + pub enum Read { + /// Total token supply for a specified token. + #[codec(index = 0)] + TotalSupply(TokenIdOf), + /// Account balance for a specified `token` and `owner`. + #[codec(index = 1)] + BalanceOf { + /// The token. + token: TokenIdOf, + /// The owner of the token. + owner: AccountIdOf, + }, + /// Allowance for a `spender` approved by an `owner`, for a specified `token`. + #[codec(index = 2)] + Allowance { + /// The token. + token: TokenIdOf, + /// The owner of the token. + owner: AccountIdOf, + /// The spender with an allowance. + spender: AccountIdOf, + }, + /// Name of the specified token. + #[codec(index = 8)] + TokenName(TokenIdOf), + /// Symbol for the specified token. + #[codec(index = 9)] + TokenSymbol(TokenIdOf), + /// Decimals for the specified token. + #[codec(index = 10)] + TokenDecimals(TokenIdOf), + /// Whether a specified token exists. + #[codec(index = 18)] + TokenExists(TokenIdOf), + } + + /// Results of state reads for the fungibles API. + #[derive(Debug)] + #[cfg_attr(feature = "std", derive(PartialEq, Clone))] + pub enum ReadResult { + /// Total token supply for a specified token. + TotalSupply(BalanceOf), + /// Account balance for a specified token and owner. + BalanceOf(BalanceOf), + /// Allowance for a spender approved by an owner, for a specified token. + Allowance(BalanceOf), + /// Name of the specified token. + TokenName(Vec), + /// Symbol for the specified token. + TokenSymbol(Vec), + /// Decimals for the specified token. + TokenDecimals(u8), + /// Whether the specified token exists. + TokenExists(bool), + } + + impl ReadResult { + /// Encodes the result. + pub fn encode(&self) -> Vec { + use ReadResult::*; + match self { + TotalSupply(result) => result.encode(), + BalanceOf(result) => result.encode(), + Allowance(result) => result.encode(), + TokenName(result) => result.encode(), + TokenSymbol(result) => result.encode(), + TokenDecimals(result) => result.encode(), + TokenExists(result) => result.encode(), + } + } + } + + /// Configure the pallet by specifying the parameters and types on which it depends. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_assets::Config { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The instance of pallet-assets. + type AssetsInstance; + /// Weight information for dispatchables in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// The events that can be emitted. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Event emitted when allowance by `owner` to `spender` changes. + // Differing style: event name abides by the PSP22 standard. + Approval { + /// The token. + token: TokenIdOf, + /// The owner providing the allowance. + owner: AccountIdOf, + /// The beneficiary of the allowance. + spender: AccountIdOf, + /// The new allowance amount. + value: BalanceOf, + }, + /// Event emitted when a token transfer occurs. + // Differing style: event name abides by the PSP22 standard. + Transfer { + /// The token. + token: TokenIdOf, + /// The source of the transfer. `None` when minting. + from: Option>, + /// The recipient of the transfer. `None` when burning. + to: Option>, + /// The amount transferred (or minted/burned). + value: BalanceOf, + }, + /// Event emitted when a token is created. + Created { + /// The token identifier. + id: TokenIdOf, + /// The creator of the token. + creator: AccountIdOf, + /// The administrator of the token. + admin: AccountIdOf, + }, + } + + #[pallet::call] + impl Pallet { + /// Transfers `value` amount of tokens from the caller's account to account `to`. + /// + /// # Parameters + /// - `token` - The token to transfer. + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + #[pallet::call_index(3)] + #[pallet::weight(AssetsWeightInfoOf::::transfer_keep_alive())] + pub fn transfer( + origin: OriginFor, + token: TokenIdOf, + to: AccountIdOf, + value: BalanceOf, + ) -> DispatchResult { + let from = ensure_signed(origin.clone())?; + AssetsOf::::transfer_keep_alive( + origin, + token.clone().into(), + T::Lookup::unlookup(to.clone()), + value, + )?; + Self::deposit_event(Event::Transfer { token, from: Some(from), to: Some(to), value }); + Ok(()) + } + + /// Transfers `value` amount tokens on behalf of `from` to account `to` with additional + /// `data` in unspecified format. + /// + /// # Parameters + /// - `token` - The token to transfer. + /// - `from` - The account from which the token balance will be withdrawn. + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + #[pallet::call_index(4)] + #[pallet::weight(AssetsWeightInfoOf::::transfer_approved())] + pub fn transfer_from( + origin: OriginFor, + token: TokenIdOf, + from: AccountIdOf, + to: AccountIdOf, + value: BalanceOf, + ) -> DispatchResult { + AssetsOf::::transfer_approved( + origin, + token.clone().into(), + T::Lookup::unlookup(from.clone()), + T::Lookup::unlookup(to.clone()), + value, + )?; + Self::deposit_event(Event::Transfer { token, from: Some(from), to: Some(to), value }); + Ok(()) + } + + /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. + /// + /// # Parameters + /// - `token` - The token to approve. + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to approve. + #[pallet::call_index(5)] + #[pallet::weight(::WeightInfo::approve(1, 1))] + pub fn approve( + origin: OriginFor, + token: TokenIdOf, + spender: AccountIdOf, + value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let owner = ensure_signed(origin.clone()) + .map_err(|e| e.with_weight(WeightOf::::approve(0, 0)))?; + let current_allowance = AssetsOf::::allowance(token.clone(), &owner, &spender); + + let weight = match value.cmp(¤t_allowance) { + // If the new value is equal to the current allowance, do nothing. + Equal => WeightOf::::approve(0, 0), + // If the new value is greater than the current allowance, approve the difference + // because `approve_transfer` works additively (see `pallet-assets`). + Greater => { + AssetsOf::::approve_transfer( + origin, + token.clone().into(), + T::Lookup::unlookup(spender.clone()), + value.saturating_sub(current_allowance), + ) + .map_err(|e| e.with_weight(WeightOf::::approve(1, 0)))?; + WeightOf::::approve(1, 0) + }, + // If the new value is less than the current allowance, cancel the approval and + // set the new value. + Less => { + let token_param: TokenIdParameterOf = token.clone().into(); + let spender_source = T::Lookup::unlookup(spender.clone()); + AssetsOf::::cancel_approval( + origin.clone(), + token_param.clone(), + spender_source.clone(), + ) + .map_err(|e| e.with_weight(WeightOf::::approve(0, 1)))?; + if value.is_zero() { + WeightOf::::approve(0, 1) + } else { + AssetsOf::::approve_transfer( + origin, + token_param, + spender_source, + value, + )?; + WeightOf::::approve(1, 1) + } + }, + }; + Self::deposit_event(Event::Approval { token, owner, spender, value }); + Ok(Some(weight).into()) + } + + /// Increases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `token` - The token to have an allowance increased. + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to increase the allowance by. + #[pallet::call_index(6)] + #[pallet::weight(::WeightInfo::approve(1, 0))] + pub fn increase_allowance( + origin: OriginFor, + token: TokenIdOf, + spender: AccountIdOf, + value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let owner = ensure_signed(origin.clone()) + .map_err(|e| e.with_weight(WeightOf::::approve(0, 0)))?; + AssetsOf::::approve_transfer( + origin, + token.clone().into(), + T::Lookup::unlookup(spender.clone()), + value, + ) + .map_err(|e| e.with_weight(AssetsWeightInfoOf::::approve_transfer()))?; + let value = AssetsOf::::allowance(token.clone(), &owner, &spender); + Self::deposit_event(Event::Approval { token, owner, spender, value }); + Ok(().into()) + } + + /// Decreases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `token` - The token to have an allowance decreased. + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to decrease the allowance by. + #[pallet::call_index(7)] + #[pallet::weight(::WeightInfo::approve(1, 1))] + pub fn decrease_allowance( + origin: OriginFor, + token: TokenIdOf, + spender: AccountIdOf, + value: BalanceOf, + ) -> DispatchResultWithPostInfo { + let owner = ensure_signed(origin.clone()) + .map_err(|e| e.with_weight(WeightOf::::approve(0, 0)))?; + if value.is_zero() { + return Ok(Some(WeightOf::::approve(0, 0)).into()); + } + let current_allowance = AssetsOf::::allowance(token.clone(), &owner, &spender); + let spender_source = T::Lookup::unlookup(spender.clone()); + let token_param: TokenIdParameterOf = token.clone().into(); + + // Cancel the approval and approve `new_allowance` if difference is more than zero. + AssetsOf::::cancel_approval( + origin.clone(), + token_param.clone(), + spender_source.clone(), + ) + .map_err(|e| e.with_weight(WeightOf::::approve(0, 1)))?; + let new_allowance = current_allowance.saturating_sub(value); + let weight = if new_allowance.is_zero() { + WeightOf::::approve(0, 1) + } else { + AssetsOf::::approve_transfer( + origin, + token_param, + spender_source, + new_allowance, + )?; + WeightOf::::approve(1, 1) + }; + Self::deposit_event(Event::Approval { token, owner, spender, value: new_allowance }); + Ok(Some(weight).into()) + } + + /// Create a new token with a given identifier. + /// + /// # Parameters + /// - `id` - The identifier of the token. + /// - `admin` - The account that will administer the token. + /// - `min_balance` - The minimum balance required for accounts holding this token. + #[pallet::call_index(11)] + #[pallet::weight(AssetsWeightInfoOf::::create())] + pub fn create( + origin: OriginFor, + id: TokenIdOf, + admin: AccountIdOf, + min_balance: BalanceOf, + ) -> DispatchResult { + let creator = ensure_signed(origin.clone())?; + AssetsOf::::create( + origin, + id.clone().into(), + T::Lookup::unlookup(admin.clone()), + min_balance, + )?; + Self::deposit_event(Event::Created { id, creator, admin }); + Ok(()) + } + + /// Start the process of destroying a token. + /// + /// # Parameters + /// - `token` - The token to be destroyed. + // See `pallet-assets` documentation for more information. Related dispatchables are: + // - `destroy_accounts` + // - `destroy_approvals` + // - `finish_destroy` + #[pallet::call_index(12)] + #[pallet::weight(AssetsWeightInfoOf::::start_destroy())] + pub fn start_destroy(origin: OriginFor, token: TokenIdOf) -> DispatchResult { + AssetsOf::::start_destroy(origin, token.into()) + } + + /// Set the metadata for a token. + /// + /// # Parameters + /// - `token`: The token to update. + /// - `name`: The user friendly name of this token. + /// - `symbol`: The exchange symbol for this token. + /// - `decimals`: The number of decimals this token uses to represent one unit. + #[pallet::call_index(16)] + #[pallet::weight(AssetsWeightInfoOf::::set_metadata(name.len() as u32, symbol.len() as u32))] + pub fn set_metadata( + origin: OriginFor, + token: TokenIdOf, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> DispatchResult { + AssetsOf::::set_metadata(origin, token.into(), name, symbol, decimals) + } + + /// Clear the metadata for a token. + /// + /// # Parameters + /// - `token` - The token to update. + #[pallet::call_index(17)] + #[pallet::weight(AssetsWeightInfoOf::::clear_metadata())] + pub fn clear_metadata(origin: OriginFor, token: TokenIdOf) -> DispatchResult { + AssetsOf::::clear_metadata(origin, token.into()) + } + + /// Creates `value` amount of tokens and assigns them to `account`, increasing the total + /// supply. + /// + /// # Parameters + /// - `token` - The token to mint. + /// - `account` - The account to be credited with the created tokens. + /// - `value` - The number of tokens to mint. + #[pallet::call_index(19)] + #[pallet::weight(AssetsWeightInfoOf::::mint())] + pub fn mint( + origin: OriginFor, + token: TokenIdOf, + account: AccountIdOf, + value: BalanceOf, + ) -> DispatchResult { + AssetsOf::::mint( + origin, + token.clone().into(), + T::Lookup::unlookup(account.clone()), + value, + )?; + Self::deposit_event(Event::Transfer { token, from: None, to: Some(account), value }); + Ok(()) + } + + /// Destroys `value` amount of tokens from `account`, reducing the total supply. + /// + /// # Parameters + /// - `token` - the token to burn. + /// - `account` - The account from which the tokens will be destroyed. + /// - `value` - The number of tokens to destroy. + #[pallet::call_index(20)] + #[pallet::weight(AssetsWeightInfoOf::::burn())] + pub fn burn( + origin: OriginFor, + token: TokenIdOf, + account: AccountIdOf, + value: BalanceOf, + ) -> DispatchResult { + AssetsOf::::burn( + origin, + token.clone().into(), + T::Lookup::unlookup(account.clone()), + value, + )?; + Self::deposit_event(Event::Transfer { token, from: Some(account), to: None, value }); + Ok(()) + } + } + + impl crate::Read for Pallet { + /// The type of read requested. + type Read = Read; + /// The type or result returned. + type Result = ReadResult; + + /// Determines the weight of the requested read, used to charge the appropriate weight + /// before the read is performed. + /// + /// # Parameters + /// - `request` - The read request. + fn weight(request: &Self::Read) -> Weight { + use Read::*; + match request { + TotalSupply(_) => ::WeightInfo::total_supply(), + BalanceOf { .. } => ::WeightInfo::balance_of(), + Allowance { .. } => ::WeightInfo::allowance(), + TokenName(_) => ::WeightInfo::token_name(), + TokenSymbol(_) => ::WeightInfo::token_symbol(), + TokenDecimals(_) => ::WeightInfo::token_decimals(), + TokenExists(_) => ::WeightInfo::token_exists(), + } + } + + /// Performs the requested read and returns the result. + /// + /// # Parameters + /// - `request` - The read request. + fn read(request: Self::Read) -> Self::Result { + use Read::*; + match request { + TotalSupply(token) => ReadResult::TotalSupply(AssetsOf::::total_supply(token)), + BalanceOf { token, owner } => + ReadResult::BalanceOf(AssetsOf::::balance(token, owner)), + Allowance { token, owner, spender } => + ReadResult::Allowance(AssetsOf::::allowance(token, &owner, &spender)), + TokenName(token) => ReadResult::TokenName( as MetadataInspect< + AccountIdOf, + >>::name(token)), + TokenSymbol(token) => ReadResult::TokenSymbol( as MetadataInspect< + AccountIdOf, + >>::symbol(token)), + TokenDecimals(token) => ReadResult::TokenDecimals( + as MetadataInspect>>::decimals(token), + ), + TokenExists(token) => ReadResult::TokenExists(AssetsOf::::asset_exists(token)), + } + } + } +} diff --git a/pallets/api/src/fungibles/tests.rs b/pallets/api/src/fungibles/tests.rs new file mode 100644 index 00000000..2e430ca8 --- /dev/null +++ b/pallets/api/src/fungibles/tests.rs @@ -0,0 +1,812 @@ +use codec::Encode; +use frame_support::{ + assert_noop, assert_ok, + dispatch::WithPostDispatchInfo, + sp_runtime::{traits::Zero, DispatchError::BadOrigin}, + traits::fungibles::{ + approvals::Inspect as ApprovalInspect, metadata::Inspect as MetadataInspect, Inspect, + }, +}; + +use crate::{ + fungibles::{ + weights::WeightInfo as WeightInfoTrait, AssetsInstanceOf, AssetsWeightInfoOf, + AssetsWeightInfoTrait, Config, Read::*, ReadResult, + }, + mock::*, + Read, +}; + +const TOKEN: u32 = 42; + +type AssetsError = pallet_assets::Error>; +type AssetsWeightInfo = AssetsWeightInfoOf; +type Event = crate::fungibles::Event; +type WeightInfo = ::WeightInfo; + +mod encoding_read_result { + use super::*; + + #[test] + fn total_supply() { + let total_supply = 1_000_000 * UNIT; + assert_eq!(ReadResult::TotalSupply::(total_supply).encode(), total_supply.encode()); + } + + #[test] + fn balance_of() { + let balance = 100 * UNIT; + assert_eq!(ReadResult::BalanceOf::(balance).encode(), balance.encode()); + } + + #[test] + fn allowance() { + let allowance = 100 * UNIT; + assert_eq!(ReadResult::Allowance::(allowance).encode(), allowance.encode()); + } + + #[test] + fn token_name() { + let name = vec![42, 42, 42, 42, 42]; + assert_eq!(ReadResult::TokenName::(name.clone()).encode(), name.encode()); + } + + #[test] + fn token_symbol() { + let symbol = vec![42, 42, 42, 42, 42]; + assert_eq!(ReadResult::TokenSymbol::(symbol.clone()).encode(), symbol.encode()); + } + + #[test] + fn token_decimals() { + let decimals = 42; + assert_eq!(ReadResult::TokenDecimals::(decimals).encode(), decimals.encode()); + } + + #[test] + fn token_exists() { + let exists = true; + assert_eq!(ReadResult::TokenExists::(exists).encode(), exists.encode()); + } +} + +#[test] +fn transfer_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let from = ALICE; + let to = BOB; + + for origin in vec![root(), none()] { + assert_noop!(Fungibles::transfer(origin, token, to, value), BadOrigin); + } + // Check error works for `Assets::transfer_keep_alive()`. + assert_noop!(Fungibles::transfer(signed(from), token, to, value), AssetsError::Unknown); + assets::create_and_mint_to(from, token, from, value * 2); + let balance_before_transfer = Assets::balance(token, &to); + assert_ok!(Fungibles::transfer(signed(from), token, to, value)); + let balance_after_transfer = Assets::balance(token, &to); + assert_eq!(balance_after_transfer, balance_before_transfer + value); + System::assert_last_event( + Event::Transfer { token, from: Some(from), to: Some(to), value }.into(), + ); + }); +} + +#[test] +fn transfer_from_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let from = ALICE; + let to = BOB; + let spender = CHARLIE; + + for origin in vec![root(), none()] { + assert_noop!(Fungibles::transfer_from(origin, token, from, to, value), BadOrigin); + } + // Check error works for `Assets::transfer_approved()`. + assert_noop!( + Fungibles::transfer_from(signed(spender), token, from, to, value), + AssetsError::Unknown + ); + // Approve `spender` to transfer up to `value`. + assets::create_mint_and_approve(spender, token, from, value * 2, spender, value); + // Successfully call transfer from. + let from_balance_before_transfer = Assets::balance(token, &from); + let to_balance_before_transfer = Assets::balance(token, &to); + assert_ok!(Fungibles::transfer_from(signed(spender), token, from, to, value)); + let from_balance_after_transfer = Assets::balance(token, &from); + let to_balance_after_transfer = Assets::balance(token, &to); + // Check that `to` has received the `value` tokens from `from`. + assert_eq!(to_balance_after_transfer, to_balance_before_transfer + value); + assert_eq!(from_balance_after_transfer, from_balance_before_transfer - value); + System::assert_last_event( + Event::Transfer { token, from: Some(from), to: Some(to), value }.into(), + ); + }); +} + +mod approve { + use super::*; + + #[test] + fn ensure_signed_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let spender = BOB; + + for origin in vec![root(), none()] { + assert_noop!( + Fungibles::approve(origin, token, spender, value), + BadOrigin.with_weight(WeightInfo::approve(0, 0)) + ); + } + }); + } + + #[test] + fn ensure_error_cases_from_pallet_assets_work() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let owner = ALICE; + let spender = BOB; + + for origin in vec![root(), none()] { + assert_noop!( + Fungibles::approve(origin, token, spender, value), + BadOrigin.with_weight(WeightInfo::approve(0, 0)) + ); + } + // Check error works for `Assets::approve_transfer()` in `Greater` match arm. + assert_noop!( + Fungibles::approve(signed(owner), token, spender, value), + AssetsError::Unknown.with_weight(WeightInfo::approve(1, 0)) + ); + assets::create_mint_and_approve(owner, token, owner, value, spender, value); + // Check error works for `Assets::cancel_approval()` in `Less` match arm. + assert_ok!(Assets::freeze_asset(signed(owner), token)); + assert_noop!( + Fungibles::approve(signed(owner), token, spender, value / 2), + AssetsError::AssetNotLive.with_weight(WeightInfo::approve(0, 1)) + ); + assert_ok!(Assets::thaw_asset(signed(owner), token)); + // No error test for `approve_transfer` in `Less` arm because it is not possible. + }); + } + + // Non-additive, sets new value. + #[test] + fn approve_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let owner = ALICE; + let spender = BOB; + + // Approves a value to spend that is higher than the current allowance. + assets::create_and_mint_to(owner, token, owner, value); + assert_eq!(Assets::allowance(token, &owner, &spender), 0); + assert_eq!( + Fungibles::approve(signed(owner), token, spender, value), + Ok(Some(WeightInfo::approve(1, 0)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), value); + System::assert_last_event(Event::Approval { token, owner, spender, value }.into()); + // Approves a value to spend that is lower than the current allowance. + assert_eq!( + Fungibles::approve(signed(owner), token, spender, value / 2), + Ok(Some(WeightInfo::approve(1, 1)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + System::assert_last_event( + Event::Approval { token, owner, spender, value: value / 2 }.into(), + ); + // Approves a value to spend that is equal to the current allowance. + assert_eq!( + Fungibles::approve(signed(owner), token, spender, value / 2), + Ok(Some(WeightInfo::approve(0, 0)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + System::assert_last_event( + Event::Approval { token, owner, spender, value: value / 2 }.into(), + ); + // Sets allowance to zero. + assert_eq!( + Fungibles::approve(signed(owner), token, spender, 0), + Ok(Some(WeightInfo::approve(0, 1)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), 0); + System::assert_last_event(Event::Approval { token, owner, spender, value: 0 }.into()); + }); + } +} + +#[test] +fn increase_allowance_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let owner = ALICE; + let spender = BOB; + + for origin in vec![root(), none()] { + assert_noop!( + Fungibles::increase_allowance(origin, token, spender, value), + BadOrigin.with_weight(WeightInfo::approve(0, 0)) + ); + } + // Check error works for `Assets::approve_transfer()`. + assert_noop!( + Fungibles::increase_allowance(signed(owner), token, spender, value), + AssetsError::Unknown.with_weight(AssetsWeightInfo::approve_transfer()) + ); + assets::create_and_mint_to(owner, token, owner, value); + assert_eq!(0, Assets::allowance(token, &owner, &spender)); + assert_ok!(Fungibles::increase_allowance(signed(owner), token, spender, value)); + assert_eq!(Assets::allowance(token, &owner, &spender), value); + System::assert_last_event(Event::Approval { token, owner, spender, value }.into()); + // Additive. + assert_ok!(Fungibles::increase_allowance(signed(owner), token, spender, value)); + assert_eq!(Assets::allowance(token, &owner, &spender), value * 2); + System::assert_last_event( + Event::Approval { token, owner, spender, value: value * 2 }.into(), + ); + }); +} + +#[test] +fn decrease_allowance_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let owner = ALICE; + let spender = BOB; + + for origin in vec![root(), none()] { + assert_noop!( + Fungibles::decrease_allowance(origin, token, spender, 0), + BadOrigin.with_weight(WeightInfo::approve(0, 0)) + ); + } + // Check error works for `Assets::cancel_approval()`. No error test for `approve_transfer` + // because it is not possible. + assert_noop!( + Fungibles::decrease_allowance(signed(owner), token, spender, value / 2), + AssetsError::Unknown.with_weight(WeightInfo::approve(0, 1)) + ); + assets::create_mint_and_approve(owner, token, owner, value, spender, value); + assert_eq!(Assets::allowance(token, &owner, &spender), value); + // Owner balance is not changed if decreased by zero. + assert_eq!( + Fungibles::decrease_allowance(signed(owner), token, spender, 0), + Ok(Some(WeightInfo::approve(0, 0)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), value); + // Decrease allowance successfully. + assert_eq!( + Fungibles::decrease_allowance(signed(owner), token, spender, value / 2), + Ok(Some(WeightInfo::approve(1, 1)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), value / 2); + System::assert_last_event( + Event::Approval { token, owner, spender, value: value / 2 }.into(), + ); + // Saturating if current allowance is decreased more than the owner balance. + assert_eq!( + Fungibles::decrease_allowance(signed(owner), token, spender, value), + Ok(Some(WeightInfo::approve(0, 1)).into()) + ); + assert_eq!(Assets::allowance(token, &owner, &spender), 0); + System::assert_last_event(Event::Approval { token, owner, spender, value: 0 }.into()); + }); +} + +#[test] +fn create_works() { + new_test_ext().execute_with(|| { + let id = TOKEN; + let creator = ALICE; + let admin = ALICE; + + for origin in vec![root(), none()] { + assert_noop!(Fungibles::create(origin, id, admin, 100), BadOrigin); + } + assert!(!Assets::asset_exists(id)); + assert_ok!(Fungibles::create(signed(creator), id, admin, 100)); + assert!(Assets::asset_exists(id)); + System::assert_last_event(Event::Created { id, creator, admin }.into()); + // Check error works for `Assets::create()`. + assert_noop!(Fungibles::create(signed(creator), id, admin, 100), AssetsError::InUse); + }); +} + +#[test] +fn start_destroy_works() { + new_test_ext().execute_with(|| { + let token = TOKEN; + + // Check error works for `Assets::start_destroy()`. + assert_noop!(Fungibles::start_destroy(signed(ALICE), token), AssetsError::Unknown); + assert_ok!(Assets::create(signed(ALICE), token, ALICE, 1)); + assert_ok!(Fungibles::start_destroy(signed(ALICE), token)); + // Check that the token is not live after starting the destroy process. + assert_noop!( + Assets::mint(signed(ALICE), token, ALICE, 10 * UNIT), + AssetsError::AssetNotLive + ); + }); +} + +#[test] +fn set_metadata_works() { + new_test_ext().execute_with(|| { + let token = TOKEN; + let name = vec![42]; + let symbol = vec![42]; + let decimals = 42; + + // Check error works for `Assets::set_metadata()`. + assert_noop!( + Fungibles::set_metadata(signed(ALICE), token, name.clone(), symbol.clone(), decimals), + AssetsError::Unknown + ); + assert_ok!(Assets::create(signed(ALICE), token, ALICE, 1)); + assert_ok!(Fungibles::set_metadata( + signed(ALICE), + token, + name.clone(), + symbol.clone(), + decimals + )); + assert_eq!(Assets::name(token), name); + assert_eq!(Assets::symbol(token), symbol); + assert_eq!(Assets::decimals(token), decimals); + }); +} + +#[test] +fn clear_metadata_works() { + new_test_ext().execute_with(|| { + let token = TOKEN; + + // Check error works for `Assets::clear_metadata()`. + assert_noop!(Fungibles::clear_metadata(signed(ALICE), token), AssetsError::Unknown); + assets::create_and_set_metadata(ALICE, token, vec![42], vec![42], 42); + assert_ok!(Fungibles::clear_metadata(signed(ALICE), token)); + assert!(Assets::name(token).is_empty()); + assert!(Assets::symbol(token).is_empty()); + assert!(Assets::decimals(token).is_zero()); + }); +} + +#[test] +fn mint_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let from = ALICE; + let to = BOB; + + // Check error works for `Assets::mint()`. + assert_noop!( + Fungibles::mint(signed(from), token, to, value), + sp_runtime::TokenError::UnknownAsset + ); + assert_ok!(Assets::create(signed(from), token, from, 1)); + let balance_before_mint = Assets::balance(token, &to); + assert_ok!(Fungibles::mint(signed(from), token, to, value)); + let balance_after_mint = Assets::balance(token, &to); + assert_eq!(balance_after_mint, balance_before_mint + value); + System::assert_last_event( + Event::Transfer { token, from: None, to: Some(to), value }.into(), + ); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + let value: Balance = 100 * UNIT; + let token = TOKEN; + let owner = ALICE; + let from = BOB; + let total_supply = value * 2; + + // Check error works for `Assets::burn()`. + assert_noop!(Fungibles::burn(signed(owner), token, from, value), AssetsError::Unknown); + assets::create_and_mint_to(owner, token, from, total_supply); + assert_eq!(Assets::total_supply(TOKEN), total_supply); + let balance_before_burn = Assets::balance(token, &from); + assert_ok!(Fungibles::burn(signed(owner), token, from, value)); + assert_eq!(Assets::total_supply(TOKEN), total_supply - value); + let balance_after_burn = Assets::balance(token, &from); + assert_eq!(balance_after_burn, balance_before_burn - value); + System::assert_last_event( + Event::Transfer { token, from: Some(from), to: None, value }.into(), + ); + }); +} + +#[test] +fn total_supply_works() { + new_test_ext().execute_with(|| { + let total_supply = INIT_AMOUNT; + assert_eq!( + Fungibles::read(TotalSupply(TOKEN)), + ReadResult::TotalSupply(Default::default()) + ); + assets::create_and_mint_to(ALICE, TOKEN, ALICE, total_supply); + assert_eq!(Fungibles::read(TotalSupply(TOKEN)), ReadResult::TotalSupply(total_supply)); + assert_eq!( + Fungibles::read(TotalSupply(TOKEN)).encode(), + Assets::total_supply(TOKEN).encode(), + ); + }); +} + +#[test] +fn balance_of_works() { + new_test_ext().execute_with(|| { + let value = 1_000 * UNIT; + assert_eq!( + Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }), + ReadResult::BalanceOf(Default::default()) + ); + assets::create_and_mint_to(ALICE, TOKEN, ALICE, value); + assert_eq!( + Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }), + ReadResult::BalanceOf(value) + ); + assert_eq!( + Fungibles::read(BalanceOf { token: TOKEN, owner: ALICE }).encode(), + Assets::balance(TOKEN, ALICE).encode(), + ); + }); +} + +#[test] +fn allowance_works() { + new_test_ext().execute_with(|| { + let value = 1_000 * UNIT; + assert_eq!( + Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }), + ReadResult::Allowance(Default::default()) + ); + assets::create_mint_and_approve(ALICE, TOKEN, ALICE, value * 2, BOB, value); + assert_eq!( + Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }), + ReadResult::Allowance(value) + ); + assert_eq!( + Fungibles::read(Allowance { token: TOKEN, owner: ALICE, spender: BOB }).encode(), + Assets::allowance(TOKEN, &ALICE, &BOB).encode(), + ); + }); +} + +#[test] +fn token_metadata_works() { + new_test_ext().execute_with(|| { + let name: Vec = vec![11, 12, 13]; + let symbol: Vec = vec![21, 22, 23]; + let decimals: u8 = 69; + assert_eq!(Fungibles::read(TokenName(TOKEN)), ReadResult::TokenName(Default::default())); + assert_eq!( + Fungibles::read(TokenSymbol(TOKEN)), + ReadResult::TokenSymbol(Default::default()) + ); + assert_eq!( + Fungibles::read(TokenDecimals(TOKEN)), + ReadResult::TokenDecimals(Default::default()) + ); + assets::create_and_set_metadata(ALICE, TOKEN, name.clone(), symbol.clone(), decimals); + assert_eq!(Fungibles::read(TokenName(TOKEN)), ReadResult::TokenName(name)); + assert_eq!(Fungibles::read(TokenSymbol(TOKEN)), ReadResult::TokenSymbol(symbol)); + assert_eq!(Fungibles::read(TokenDecimals(TOKEN)), ReadResult::TokenDecimals(decimals)); + assert_eq!(Fungibles::read(TokenName(TOKEN)).encode(), Assets::name(TOKEN).encode()); + assert_eq!(Fungibles::read(TokenSymbol(TOKEN)).encode(), Assets::symbol(TOKEN).encode()); + assert_eq!( + Fungibles::read(TokenDecimals(TOKEN)).encode(), + Assets::decimals(TOKEN).encode(), + ); + }); +} + +#[test] +fn token_exists_works() { + new_test_ext().execute_with(|| { + assert_eq!(Fungibles::read(TokenExists(TOKEN)), ReadResult::TokenExists(false)); + assert_ok!(Assets::create(signed(ALICE), TOKEN, ALICE, 1)); + assert_eq!(Fungibles::read(TokenExists(TOKEN)), ReadResult::TokenExists(true)); + assert_eq!( + Fungibles::read(TokenExists(TOKEN)).encode(), + Assets::asset_exists(TOKEN).encode(), + ); + }); +} + +fn signed(account: AccountId) -> RuntimeOrigin { + RuntimeOrigin::signed(account) +} + +fn root() -> RuntimeOrigin { + RuntimeOrigin::root() +} + +fn none() -> RuntimeOrigin { + RuntimeOrigin::none() +} + +// Helper functions for interacting with pallet-assets. +mod assets { + use super::*; + + pub(super) fn create_and_mint_to( + owner: AccountId, + token: TokenId, + to: AccountId, + value: Balance, + ) { + assert_ok!(Assets::create(signed(owner), token, owner, 1)); + assert_ok!(Assets::mint(signed(owner), token, to, value)); + } + + pub(super) fn create_mint_and_approve( + owner: AccountId, + token: TokenId, + to: AccountId, + mint: Balance, + spender: AccountId, + approve: Balance, + ) { + create_and_mint_to(owner, token, to, mint); + assert_ok!(Assets::approve_transfer(signed(to), token, spender, approve,)); + } + + pub(super) fn create_and_set_metadata( + owner: AccountId, + token: TokenId, + name: Vec, + symbol: Vec, + decimals: u8, + ) { + assert_ok!(Assets::create(signed(owner), token, owner, 1)); + assert_ok!(Assets::set_metadata(signed(owner), token, name, symbol, decimals)); + } +} + +mod read_weights { + use frame_support::weights::Weight; + + use super::*; + use crate::fungibles::{weights::WeightInfo, Config}; + + struct ReadWeightInfo { + total_supply: Weight, + balance_of: Weight, + allowance: Weight, + token_name: Weight, + token_symbol: Weight, + token_decimals: Weight, + token_exists: Weight, + } + + impl ReadWeightInfo { + fn new() -> Self { + Self { + total_supply: Fungibles::weight(&TotalSupply(TOKEN)), + balance_of: Fungibles::weight(&BalanceOf { token: TOKEN, owner: ALICE }), + allowance: Fungibles::weight(&Allowance { + token: TOKEN, + owner: ALICE, + spender: BOB, + }), + token_name: Fungibles::weight(&TokenName(TOKEN)), + token_symbol: Fungibles::weight(&TokenSymbol(TOKEN)), + token_decimals: Fungibles::weight(&TokenDecimals(TOKEN)), + token_exists: Fungibles::weight(&TokenExists(TOKEN)), + } + } + } + + #[test] + fn ensure_read_matches_benchmarks() { + let ReadWeightInfo { + allowance, + balance_of, + token_decimals, + token_name, + token_symbol, + total_supply, + token_exists, + } = ReadWeightInfo::new(); + + assert_eq!(total_supply, ::WeightInfo::total_supply()); + assert_eq!(balance_of, ::WeightInfo::balance_of()); + assert_eq!(allowance, ::WeightInfo::allowance()); + assert_eq!(token_name, ::WeightInfo::token_name()); + assert_eq!(token_symbol, ::WeightInfo::token_symbol()); + assert_eq!(token_decimals, ::WeightInfo::token_decimals()); + assert_eq!(token_exists, ::WeightInfo::token_exists()); + } + + // These types read from the `AssetMetadata` storage. + #[test] + fn ensure_asset_metadata_variants_match() { + let ReadWeightInfo { token_decimals, token_name, token_symbol, .. } = ReadWeightInfo::new(); + + assert_eq!(token_decimals, token_name); + assert_eq!(token_decimals, token_symbol); + } + + // These types read from the `Assets` storage. + #[test] + fn ensure_asset_variants_match() { + let ReadWeightInfo { total_supply, token_exists, .. } = ReadWeightInfo::new(); + + assert_eq!(total_supply, token_exists); + } + + // Proof size is based on `MaxEncodedLen`, not hardware. + // This test ensures that the data structure sizes do not change with upgrades. + #[test] + fn ensure_expected_proof_size_does_not_change() { + let ReadWeightInfo { + allowance, + balance_of, + token_decimals, + token_name, + token_symbol, + total_supply, + token_exists, + } = ReadWeightInfo::new(); + + // These values come from `weights.rs`. + assert_eq!(allowance.proof_size(), 3613); + assert_eq!(balance_of.proof_size(), 3599); + assert_eq!(token_name.proof_size(), 3605); + assert_eq!(token_symbol.proof_size(), 3605); + assert_eq!(token_decimals.proof_size(), 3605); + assert_eq!(total_supply.proof_size(), 3675); + assert_eq!(token_exists.proof_size(), 3675); + } +} + +mod ensure_codec_indexes { + use super::{Encode, RuntimeCall, *}; + use crate::{fungibles, fungibles::Call::*, mock::RuntimeCall::Fungibles}; + + #[test] + fn ensure_read_variant_indexes() { + [ + (TotalSupply::(Default::default()), 0u8, "TotalSupply"), + ( + BalanceOf:: { token: Default::default(), owner: Default::default() }, + 1, + "BalanceOf", + ), + ( + Allowance:: { + token: Default::default(), + owner: Default::default(), + spender: Default::default(), + }, + 2, + "Allowance", + ), + (TokenName::(Default::default()), 8, "TokenName"), + (TokenSymbol::(Default::default()), 9, "TokenSymbol"), + (TokenDecimals::(Default::default()), 10, "TokenDecimals"), + (TokenExists::(Default::default()), 18, "TokenExists"), + ] + .iter() + .for_each(|(variant, expected_index, name)| { + assert_eq!(variant.encode()[0], *expected_index, "{name} variant index changed"); + }) + } + + #[test] + fn ensure_dispatchable_indexes() { + use fungibles::Call::*; + + [ + ( + transfer { + token: Default::default(), + to: Default::default(), + value: Default::default(), + }, + 3u8, + "transfer", + ), + ( + transfer_from { + token: Default::default(), + from: Default::default(), + to: Default::default(), + value: Default::default(), + }, + 4, + "transfer_from", + ), + ( + approve { + token: Default::default(), + spender: Default::default(), + value: Default::default(), + }, + 5, + "approve", + ), + ( + increase_allowance { + token: Default::default(), + spender: Default::default(), + value: Default::default(), + }, + 6, + "increase_allowance", + ), + ( + decrease_allowance { + token: Default::default(), + spender: Default::default(), + value: Default::default(), + }, + 7, + "decrease_allowance", + ), + ( + create { + id: Default::default(), + admin: Default::default(), + min_balance: Default::default(), + }, + 11, + "create", + ), + (start_destroy { token: Default::default() }, 12, "start_destroy"), + ( + set_metadata { + token: Default::default(), + name: Default::default(), + symbol: Default::default(), + decimals: Default::default(), + }, + 16, + "set_metadata", + ), + (clear_metadata { token: Default::default() }, 17, "clear_metadata"), + ( + mint { + token: Default::default(), + account: Default::default(), + value: Default::default(), + }, + 19, + "mint", + ), + ( + burn { + token: Default::default(), + account: Default::default(), + value: Default::default(), + }, + 20, + "burn", + ), + ] + .iter() + .for_each(|(variant, expected_index, name)| { + assert_eq!( + Fungibles(variant.to_owned()).encode()[1], + *expected_index, + "{name} dispatchable index changed" + ); + }) + } +} diff --git a/pallets/api/src/fungibles/weights.rs b/pallets/api/src/fungibles/weights.rs new file mode 100644 index 00000000..28c5a6b5 --- /dev/null +++ b/pallets/api/src/fungibles/weights.rs @@ -0,0 +1,239 @@ + +//! Autogenerated weights for `fungibles` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 40.0.0 +//! DATE: 2024-09-13, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `R0GUE`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/pop-node +// benchmark +// pallet +// --chain=dev +// --wasm-execution=compiled +// --pallet=fungibles +// --steps=50 +// --repeat=20 +// --json +// --template +// ./scripts/pallet-weights-template.hbs +// --output=./pallets/api/src/fungibles/weights.rs +// --extrinsic= + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `fungibles`. +pub trait WeightInfo { + fn approve(a: u32, c: u32, ) -> Weight; + fn total_supply() -> Weight; + fn balance_of() -> Weight; + fn allowance() -> Weight; + fn token_name() -> Weight; + fn token_symbol() -> Weight; + fn token_decimals() -> Weight; + fn token_exists() -> Weight; +} + +/// Weights for `fungibles` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Assets::Approvals` (r:1 w:1) + /// Proof: `Assets::Approvals` (`max_values`: None, `max_size`: Some(148), added: 2623, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `a` is `[0, 1]`. + /// The range of component `c` is `[0, 1]`. + fn approve(a: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `413 + c * (102 ±0)` + // Estimated: `3675` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(1_473_469, 3675) + // Standard Error: 89_329 + .saturating_add(Weight::from_parts(19_606_122, 0).saturating_mul(a.into())) + // Standard Error: 89_329 + .saturating_add(Weight::from_parts(30_920_408, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(2_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(c.into()))) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + fn total_supply() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3675` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3675) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn balance_of() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3599` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_000_000, 3599) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Approvals` (r:1 w:0) + /// Proof: `Assets::Approvals` (`max_values`: None, `max_size`: Some(148), added: 2623, mode: `MaxEncodedLen`) + fn allowance() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3613` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(4_000_000, 3613) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_name() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_symbol() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_decimals() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + fn token_exists() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3675` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3675) + .saturating_add(T::DbWeight::get().reads(1_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Assets::Approvals` (r:1 w:1) + /// Proof: `Assets::Approvals` (`max_values`: None, `max_size`: Some(148), added: 2623, mode: `MaxEncodedLen`) + /// Storage: `Assets::Asset` (r:1 w:1) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `a` is `[0, 1]`. + /// The range of component `c` is `[0, 1]`. + fn approve(a: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `413 + c * (102 ±0)` + // Estimated: `3675` + // Minimum execution time: 20_000_000 picoseconds. + Weight::from_parts(1_473_469, 3675) + // Standard Error: 89_329 + .saturating_add(Weight::from_parts(19_606_122, 0).saturating_mul(a.into())) + // Standard Error: 89_329 + .saturating_add(Weight::from_parts(30_920_408, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(c.into()))) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + fn total_supply() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3675` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3675) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Account` (r:1 w:0) + /// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`) + fn balance_of() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3599` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(3_000_000, 3599) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Approvals` (r:1 w:0) + /// Proof: `Assets::Approvals` (`max_values`: None, `max_size`: Some(148), added: 2623, mode: `MaxEncodedLen`) + fn allowance() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3613` + // Minimum execution time: 4_000_000 picoseconds. + Weight::from_parts(4_000_000, 3613) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_name() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_symbol() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 2_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Metadata` (r:1 w:0) + /// Proof: `Assets::Metadata` (`max_values`: None, `max_size`: Some(140), added: 2615, mode: `MaxEncodedLen`) + fn token_decimals() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3605` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3605) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } + /// Storage: `Assets::Asset` (r:1 w:0) + /// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`) + fn token_exists() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `3675` + // Minimum execution time: 1_000_000 picoseconds. + Weight::from_parts(2_000_000, 3675) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + } +} + diff --git a/pallets/api/src/lib.rs b/pallets/api/src/lib.rs new file mode 100644 index 00000000..d94d1978 --- /dev/null +++ b/pallets/api/src/lib.rs @@ -0,0 +1,30 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use extension::Extension; +use frame_support::pallet_prelude::Weight; + +pub mod extension; +pub mod fungibles; +#[cfg(test)] +mod mock; + +/// Trait for performing reads of runtime state. +pub trait Read { + /// The type of read requested. + type Read; + /// The type or result returned. + type Result; + + /// Determines the weight of the requested read, used to charge the appropriate weight before + /// the read is performed. + /// + /// # Parameters + /// - `request` - The read request. + fn weight(read: &Self::Read) -> Weight; + + /// Performs the requested read and returns the result. + /// + /// # Parameters + /// - `request` - The read request. + fn read(request: Self::Read) -> Self::Result; +} diff --git a/pallets/api/src/mock.rs b/pallets/api/src/mock.rs new file mode 100644 index 00000000..42c8bf0e --- /dev/null +++ b/pallets/api/src/mock.rs @@ -0,0 +1,127 @@ +use frame_support::{ + derive_impl, parameter_types, + traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, Everything}, +}; +use frame_system::{EnsureRoot, EnsureSigned}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub(crate) const ALICE: AccountId = 1; +pub(crate) const BOB: AccountId = 2; +pub(crate) const CHARLIE: AccountId = 3; +pub(crate) const INIT_AMOUNT: Balance = 100_000_000 * UNIT; +pub(crate) const UNIT: Balance = 10_000_000_000; + +type Block = frame_system::mocking::MockBlock; +pub(crate) type AccountId = u64; +pub(crate) type Balance = u128; +// For terminology in tests. +pub(crate) type TokenId = u32; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Assets: pallet_assets::, + Balances: pallet_balances, + Fungibles: crate::fungibles, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId; + type BaseCallFilter = Everything; + type Block = Block; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = SS58Prefix; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type FreezeIdentifier = (); + type MaxFreezes = ConstU32<0>; + type MaxLocks = (); + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeHoldReason = RuntimeHoldReason; + type WeightInfo = (); +} + +type AssetsInstance = pallet_assets::Instance1; +impl pallet_assets::Config for Test { + type ApprovalDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<10>; + type AssetDeposit = ConstU128<1>; + type AssetId = TokenId; + type AssetIdParameter = TokenId; + type Balance = Balance; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); + type CallbackHandle = (); + type CreateOrigin = AsEnsureOriginWithArg>; + type Currency = Balances; + type Extra = (); + type ForceOrigin = EnsureRoot; + type Freezer = (); + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type RemoveItemsLimit = ConstU32<5>; + type RuntimeEvent = RuntimeEvent; + type StringLimit = ConstU32<50>; + type WeightInfo = (); +} + +impl crate::fungibles::Config for Test { + type AssetsInstance = AssetsInstance; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT), (CHARLIE, INIT_AMOUNT)], + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/polkadot-launch/config.json b/polkadot-launch/config.json deleted file mode 100644 index f03f983a..00000000 --- a/polkadot-launch/config.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "relaychain": { - "bin": "../../polkadot/target/release/polkadot", - "chain": "rococo-local", - "nodes": [ - { - "name": "alice", - "wsPort": 9944, - "port": 30444 - }, - { - "name": "bob", - "wsPort": 9955, - "port": 30555 - } - ] - }, - "parachains": [ - { - "bin": "../target/release/polkadot-parachain", - "id": "200", - "balance": "1000000000000000000000", - "nodes": [ - { - "wsPort": 9988, - "name": "alice", - "port": 31200, - "flags": [ - "--force-authoring", - "--", - "--execution=wasm" - ] - } - ] - } - ], - "types": { - } -} diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index f82884da..4caf8eaa 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -1,19 +1,18 @@ [package] -description = "Easily access the power of Polkadot via the Pop Network" +description = "Enabling smart(er) contracts with the power of Polkadot" edition = "2021" license = "GPL-3.0-only" name = "pop-api" version = "0.0.0" [dependencies] -enumflags2 = { version = "0.7.7" } ink = { version = "5.0.0", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ] } -sp-io = { version = "23.0.0", default-features = false, features = [ "disable_allocator", "disable_oom", "disable_panic_handler" ] } -sp-runtime = { version = "24.0", default-features = false } - pop-primitives = { path = "../primitives", default-features = false } +sp-io = { version = "37.0.0", default-features = false, features = [ + "disable_allocator", + "disable_oom", + "disable_panic_handler", +] } [lib] crate-type = [ "rlib" ] @@ -22,12 +21,5 @@ path = "src/lib.rs" [features] default = [ "std" ] -std = [ - "enumflags2/std", - "ink/std", - "pop-primitives/std", - "scale-info/std", - "scale/std", - "sp-io/std", - "sp-runtime/std", -] +fungibles = [ ] +std = [ "ink/std", "pop-primitives/std", "sp-io/std" ] diff --git a/pop-api/examples/read-runtime-state/.gitignore b/pop-api/examples/.gitignore similarity index 91% rename from pop-api/examples/read-runtime-state/.gitignore rename to pop-api/examples/.gitignore index 8de8f877..d60800c8 100755 --- a/pop-api/examples/read-runtime-state/.gitignore +++ b/pop-api/examples/.gitignore @@ -1,9 +1,9 @@ # Ignore build artifacts from the local tests sub-crate. -/target/ +**/target/ # Ignore backup files creates by cargo fmt. **/*.rs.bk # Remove Cargo.lock when creating an executable, leave it for libraries # More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock +**/Cargo.lock diff --git a/pop-api/examples/balance-transfer/.gitignore b/pop-api/examples/balance-transfer/.gitignore deleted file mode 100755 index 8de8f877..00000000 --- a/pop-api/examples/balance-transfer/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore build artifacts from the local tests sub-crate. -/target/ - -# Ignore backup files creates by cargo fmt. -**/*.rs.bk - -# Remove Cargo.lock when creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock diff --git a/pop-api/examples/balance-transfer/lib.rs b/pop-api/examples/balance-transfer/lib.rs index 328a818b..e75c15b9 100755 --- a/pop-api/examples/balance-transfer/lib.rs +++ b/pop-api/examples/balance-transfer/lib.rs @@ -1,3 +1,4 @@ +// DEPRECATED #![cfg_attr(not(feature = "std"), no_std, no_main)] use pop_api::balances::*; @@ -14,7 +15,7 @@ impl From for ContractError { } } -#[ink::contract(env = pop_api::Environment)] +#[ink::contract] mod balance_transfer { use super::*; @@ -131,4 +132,4 @@ mod balance_transfer { Ok(()) } } -} \ No newline at end of file +} diff --git a/pop-api/examples/fungibles/Cargo.toml b/pop-api/examples/fungibles/Cargo.toml new file mode 100755 index 00000000..0b79e1b2 --- /dev/null +++ b/pop-api/examples/fungibles/Cargo.toml @@ -0,0 +1,21 @@ +[package] +authors = [ "[your_name] <[your_email]>" ] +edition = "2021" +name = "fungibles" +version = "0.1.0" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +pop-api = { path = "../../../pop-api", default-features = false, features = [ "fungibles" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs new file mode 100755 index 00000000..11eafe21 --- /dev/null +++ b/pop-api/examples/fungibles/lib.rs @@ -0,0 +1,108 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use ink::prelude::vec::Vec; +use pop_api::{ + fungibles::{self as api}, + primitives::TokenId, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod fungibles { + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Fungibles; + + impl Fungibles { + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn total_supply(&self, token: TokenId) -> Result { + api::total_supply(token) + } + + #[ink(message)] + pub fn balance_of(&self, token: TokenId, owner: AccountId) -> Result { + api::balance_of(token, owner) + } + + #[ink(message)] + pub fn allowance( + &self, + token: TokenId, + owner: AccountId, + spender: AccountId, + ) -> Result { + api::allowance(token, owner, spender) + } + + #[ink(message)] + pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { + api::transfer(token, to, value) + } + + #[ink(message)] + pub fn transfer_from( + &mut self, + token: TokenId, + from: AccountId, + to: AccountId, + value: Balance, + _data: Vec, + ) -> Result<()> { + api::transfer_from(token, from, to, value) + } + + #[ink(message)] + pub fn approve( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::approve(token, spender, value) + } + + #[ink(message)] + pub fn increase_allowance( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::increase_allowance(token, spender, value) + } + + #[ink(message)] + pub fn decrease_allowance( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::decrease_allowance(token, spender, value) + } + + #[ink(message)] + pub fn token_name(&self, token: TokenId) -> Result> { + api::token_name(token) + } + + #[ink(message)] + pub fn token_symbol(&self, token: TokenId) -> Result> { + api::token_symbol(token) + } + + #[ink(message)] + pub fn token_decimals(&self, token: TokenId) -> Result { + api::token_decimals(token) + } + } +} diff --git a/pop-api/examples/nfts/.gitignore b/pop-api/examples/nfts/.gitignore deleted file mode 100755 index 8de8f877..00000000 --- a/pop-api/examples/nfts/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore build artifacts from the local tests sub-crate. -/target/ - -# Ignore backup files creates by cargo fmt. -**/*.rs.bk - -# Remove Cargo.lock when creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock diff --git a/pop-api/examples/nfts/lib.rs b/pop-api/examples/nfts/lib.rs index d47140e7..0cd0f313 100755 --- a/pop-api/examples/nfts/lib.rs +++ b/pop-api/examples/nfts/lib.rs @@ -1,3 +1,4 @@ +// DEPRECATED #![cfg_attr(not(feature = "std"), no_std, no_main)] use pop_api::nfts::*; @@ -17,7 +18,7 @@ impl From for ContractError { } } -#[ink::contract(env = pop_api::Environment)] +#[ink::contract] mod nfts { use super::*; @@ -33,27 +34,29 @@ mod nfts { } #[ink(message)] - pub fn create_nft_collection( &self ) -> Result<(), ContractError>{ + pub fn create_nft_collection(&self) -> Result<(), ContractError> { ink::env::debug_println!("Nfts::create_nft_collection: collection creation started."); - let admin = Self::env().caller(); - let item_settings = ItemSettings(BitFlags::from(ItemSetting::Transferable)); - - let mint_settings = MintSettings { - mint_type: MintType::Issuer, - price: Some(0), - start_block: Some(0), - end_block: Some(0), - default_item_settings: item_settings, - }; - - let config = CollectionConfig { - settings: CollectionSettings(BitFlags::from(CollectionSetting::TransferableItems)), - max_supply: None, - mint_settings, - }; - pop_api::nfts::create(admin, config)?; - ink::env::debug_println!("Nfts::create_nft_collection: collection created successfully."); - Ok(()) + let admin = Self::env().caller(); + let item_settings = ItemSettings(BitFlags::from(ItemSetting::Transferable)); + + let mint_settings = MintSettings { + mint_type: MintType::Issuer, + price: Some(0), + start_block: Some(0), + end_block: Some(0), + default_item_settings: item_settings, + }; + + let config = CollectionConfig { + settings: CollectionSettings(BitFlags::from(CollectionSetting::TransferableItems)), + max_supply: None, + mint_settings, + }; + pop_api::nfts::create(admin, config)?; + ink::env::debug_println!( + "Nfts::create_nft_collection: collection created successfully." + ); + Ok(()) } #[ink(message)] @@ -82,9 +85,7 @@ mod nfts { // check owner match owner(collection_id, item_id)? { Some(owner) if owner == receiver => { - ink::env::debug_println!( - "Nfts::mint success: minted item belongs to receiver" - ); + ink::env::debug_println!("Nfts::mint success: minted item belongs to receiver"); }, _ => { return Err(ContractError::NotOwner); @@ -113,4 +114,4 @@ mod nfts { Nfts::new(); } } -} \ No newline at end of file +} diff --git a/pop-api/examples/place-spot-order/.gitignore b/pop-api/examples/place-spot-order/.gitignore deleted file mode 100755 index 8de8f877..00000000 --- a/pop-api/examples/place-spot-order/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Ignore build artifacts from the local tests sub-crate. -/target/ - -# Ignore backup files creates by cargo fmt. -**/*.rs.bk - -# Remove Cargo.lock when creating an executable, leave it for libraries -# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock -Cargo.lock diff --git a/pop-api/examples/place-spot-order/lib.rs b/pop-api/examples/place-spot-order/lib.rs index f5e34f7f..965917d1 100755 --- a/pop-api/examples/place-spot-order/lib.rs +++ b/pop-api/examples/place-spot-order/lib.rs @@ -1,6 +1,7 @@ +// DEPRECATED #![cfg_attr(not(feature = "std"), no_std, no_main)] -#[ink::contract(env = pop_api::Environment)] +#[ink::contract] mod spot_order { #[ink(storage)] @@ -15,11 +16,7 @@ mod spot_order { } #[ink(message)] - pub fn place_spot_order( - &mut self, - max_amount: Balance, - para_id: u32, - ) { + pub fn place_spot_order(&mut self, max_amount: Balance, para_id: u32) { ink::env::debug_println!( "SpotOrder::place_spot_order: max_amount {:?} para_id: {:?} ", max_amount, @@ -28,10 +25,7 @@ mod spot_order { #[allow(unused_variables)] let res = pop_api::cross_chain::coretime::place_spot_order(max_amount, para_id); - ink::env::debug_println!( - "SpotOrder::place_spot_order: res {:?} ", - res, - ); + ink::env::debug_println!("SpotOrder::place_spot_order: res {:?} ", res,); ink::env::debug_println!("SpotOrder::place_spot_order end"); } @@ -46,4 +40,4 @@ mod spot_order { SpotOrder::new(); } } -} \ No newline at end of file +} diff --git a/pop-api/examples/read-runtime-state/lib.rs b/pop-api/examples/read-runtime-state/lib.rs index 05a44108..092e9f2f 100755 --- a/pop-api/examples/read-runtime-state/lib.rs +++ b/pop-api/examples/read-runtime-state/lib.rs @@ -1,35 +1,36 @@ +// DEPRECATED #![cfg_attr(not(feature = "std"), no_std, no_main)] -#[ink::contract(env = pop_api::Environment)] +#[ink::contract] mod read_relay_blocknumber { - use pop_api::primitives::storage_keys::{ - ParachainSystemKeys::LastRelayChainBlockNumber, RuntimeStateKeys::ParachainSystem, - }; + use pop_api::primitives::storage_keys::{ + ParachainSystemKeys::LastRelayChainBlockNumber, RuntimeStateKeys::ParachainSystem, + }; - #[ink(event)] - pub struct RelayBlockNumberRead { - value: BlockNumber, - } + #[ink(event)] + pub struct RelayBlockNumberRead { + value: BlockNumber, + } - #[ink(storage)] - #[derive(Default)] - pub struct ReadRelayBlockNumber; + #[ink(storage)] + #[derive(Default)] + pub struct ReadRelayBlockNumber; - impl ReadRelayBlockNumber { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("ReadRelayBlockNumber::new"); - Default::default() - } + impl ReadRelayBlockNumber { + #[ink(constructor, payable)] + pub fn new() -> Self { + ink::env::debug_println!("ReadRelayBlockNumber::new"); + Default::default() + } - #[ink(message)] - pub fn read_relay_block_number(&self) { - let result = - pop_api::state::read::(ParachainSystem(LastRelayChainBlockNumber)); - ink::env::debug_println!("Last relay block number read by contract: {:?}", result); - self.env().emit_event(RelayBlockNumberRead { - value: result.expect("Failed to read relay block number."), - }); - } - } -} \ No newline at end of file + #[ink(message)] + pub fn read_relay_block_number(&self) { + let result = + pop_api::state::read::(ParachainSystem(LastRelayChainBlockNumber)); + ink::env::debug_println!("Last relay block number read by contract: {:?}", result); + self.env().emit_event(RelayBlockNumberRead { + value: result.expect("Failed to read relay block number."), + }); + } + } +} diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml new file mode 100644 index 00000000..cad7b4c4 --- /dev/null +++ b/pop-api/integration-tests/Cargo.toml @@ -0,0 +1,44 @@ +[package] +build = "build.rs" +edition = "2021" +name = "integration-tests" +version = "0.1.0" + +[build-dependencies] +contract-build = "4.1.1" + +[dev-dependencies] +env_logger = "0.11.2" +frame-support = { version = "36.0.0", default-features = false } +frame-support-procedural = { version = "=30.0.1", default-features = false } +frame-system = { version = "36.1.0", default-features = false } +log = "0.4.22" +pallet-assets = { version = "37.0.0", default-features = false } +pallet-balances = { version = "37.0.0", default-features = false } +pallet-contracts = { version = "35.0.0", default-features = false } +pop-api = { path = "../../pop-api", default-features = false, features = [ + "fungibles", +] } +pop-primitives = { path = "../../primitives", default-features = false } +pop-runtime-devnet = { path = "../../runtime/devnet", default-features = false } +scale = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [ + "derive", +] } +sp-io = { version = "37.0.0", default-features = false } +sp-runtime = { version = "=38.0.0", default-features = false } + +[features] +default = [ "std" ] +std = [ + "frame-support/std", + "frame-system/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-contracts/std", + "pop-api/std", + "pop-primitives/std", + "pop-runtime-devnet/std", + "scale/std", + "sp-io/std", + "sp-runtime/std", +] diff --git a/pop-api/integration-tests/build.rs b/pop-api/integration-tests/build.rs new file mode 100644 index 00000000..21334ac2 --- /dev/null +++ b/pop-api/integration-tests/build.rs @@ -0,0 +1,58 @@ +use std::{ + fs, + path::{Path, PathBuf}, + process, +}; + +use contract_build::{ + execute, BuildArtifacts, BuildMode, BuildResult, ExecuteArgs, ManifestPath, OutputType, + Verbosity, +}; + +fn main() { + let contracts_dir = PathBuf::from("./contracts/"); + let contract_dirs = match get_subcontract_directories(&contracts_dir) { + Ok(dirs) => dirs, + Err(e) => { + eprintln!("Failed to read contracts directory: {}", e); + process::exit(1); + }, + }; + + for contract in contract_dirs { + if let Err(e) = build_contract(&contract) { + eprintln!("Failed to build contract {}: {}", contract.display(), e); + process::exit(1); + } + } +} + +// Function to retrieve all subdirectories in a given directory. +fn get_subcontract_directories(contracts_dir: &Path) -> Result, String> { + fs::read_dir(contracts_dir) + .map_err(|e| format!("Could not read directory '{}': {}", contracts_dir.display(), e))? + .filter_map(|entry| match entry { + Ok(entry) if entry.path().is_dir() => Some(Ok(entry.path())), + Ok(_) => None, + Err(e) => Some(Err(format!("Error reading directory entry: {}", e))), + }) + .collect() +} + +// Function to build a contract given its directory. +fn build_contract(contract_dir: &Path) -> Result { + let manifest_path = ManifestPath::new(contract_dir.join("Cargo.toml")).map_err(|e| { + format!("Could not retrieve manifest path for {}: {}", contract_dir.display(), e) + })?; + + let args = ExecuteArgs { + build_artifact: BuildArtifacts::CodeOnly, + build_mode: BuildMode::Debug, + manifest_path, + output_type: OutputType::HumanReadable, + verbosity: Verbosity::Default, + ..Default::default() + }; + + execute(args).map_err(|e| format!("Build failed for {}: {}", contract_dir.display(), e)) +} diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml new file mode 100755 index 00000000..a1517cdd --- /dev/null +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2021" +name = "create_token_in_constructor" +version = "0.1.0" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +pop-api = { path = "../../..", default-features = false, features = [ "fungibles" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs new file mode 100755 index 00000000..3ca787be --- /dev/null +++ b/pop-api/integration-tests/contracts/create_token_in_constructor/lib.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +use pop_api::{ + fungibles::{self as api, events::Created}, + primitives::TokenId, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod create_token_in_constructor { + use super::*; + + #[ink(storage)] + pub struct Fungible { + token: TokenId, + } + + impl Fungible { + #[ink(constructor, payable)] + pub fn new(id: TokenId, min_balance: Balance) -> Result { + let contract = Self { token: id }; + // AccountId of the contract which will be set to the owner of the fungible token. + let owner = contract.env().account_id(); + api::create(id, owner, min_balance)?; + contract.env().emit_event(Created { id, creator: owner, admin: owner }); + Ok(contract) + } + + #[ink(message)] + pub fn token_exists(&self) -> Result { + api::token_exists(self.token) + } + } +} diff --git a/pop-api/integration-tests/contracts/fungibles/Cargo.toml b/pop-api/integration-tests/contracts/fungibles/Cargo.toml new file mode 100755 index 00000000..dce9b257 --- /dev/null +++ b/pop-api/integration-tests/contracts/fungibles/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition = "2021" +name = "fungibles" +version = "0.1.0" + +[dependencies] +ink = { version = "5.0.0", default-features = false } +pop-api = { path = "../../../../pop-api", default-features = false, features = [ "fungibles" ] } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +e2e-tests = [ ] +ink-as-dependency = [ ] +std = [ + "ink/std", + "pop-api/std", +] diff --git a/pop-api/integration-tests/contracts/fungibles/lib.rs b/pop-api/integration-tests/contracts/fungibles/lib.rs new file mode 100755 index 00000000..17dda2af --- /dev/null +++ b/pop-api/integration-tests/contracts/fungibles/lib.rs @@ -0,0 +1,218 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// 1. PSP-22 +/// 2. PSP-22 Metadata +/// 3. Management +/// 4. PSP-22 Mintable & Burnable +use ink::prelude::vec::Vec; +use pop_api::{ + fungibles::{ + self as api, + events::{Approval, Created, DestroyStarted, MetadataCleared, MetadataSet, Transfer}, + }, + primitives::TokenId, + StatusCode, +}; + +pub type Result = core::result::Result; + +#[ink::contract] +mod fungibles { + use super::*; + + #[ink(storage)] + #[derive(Default)] + pub struct Fungibles; + + impl Fungibles { + #[ink(constructor, payable)] + pub fn new() -> Self { + ink::env::debug_println!("PopApiFungiblesExample::new"); + Default::default() + } + + /// 1. PSP-22 Interface: + /// - total_supply + /// - balance_of + /// - allowance + /// - transfer + /// - transfer_from + /// - approve + /// - increase_allowance + /// - decrease_allowance + + #[ink(message)] + pub fn total_supply(&self, token: TokenId) -> Result { + api::total_supply(token) + } + + #[ink(message)] + pub fn balance_of(&self, token: TokenId, owner: AccountId) -> Result { + api::balance_of(token, owner) + } + + #[ink(message)] + pub fn allowance( + &self, + token: TokenId, + owner: AccountId, + spender: AccountId, + ) -> Result { + api::allowance(token, owner, spender) + } + + #[ink(message)] + pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { + api::transfer(token, to, value)?; + self.env().emit_event(Transfer { + from: Some(self.env().account_id()), + to: Some(to), + value, + }); + Ok(()) + } + + #[ink(message)] + pub fn transfer_from( + &mut self, + token: TokenId, + from: AccountId, + to: AccountId, + value: Balance, + // In the PSP-22 standard a `[u8]`, but the size needs to be known at compile time. + _data: Vec, + ) -> Result<()> { + api::transfer_from(token, from, to, value)?; + self.env().emit_event(Transfer { from: Some(from), to: Some(to), value }); + Ok(()) + } + + #[ink(message)] + pub fn approve( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::approve(token, spender, value)?; + self.env() + .emit_event(Approval { owner: self.env().account_id(), spender, value }); + Ok(()) + } + + #[ink(message)] + pub fn increase_allowance( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::increase_allowance(token, spender, value) + } + + #[ink(message)] + pub fn decrease_allowance( + &mut self, + token: TokenId, + spender: AccountId, + value: Balance, + ) -> Result<()> { + api::decrease_allowance(token, spender, value) + } + + /// 2. PSP-22 Metadata Interface: + /// - token_name + /// - token_symbol + /// - token_decimals + + #[ink(message)] + pub fn token_name(&self, token: TokenId) -> Result> { + api::token_name(token) + } + + #[ink(message)] + pub fn token_symbol(&self, token: TokenId) -> Result> { + api::token_symbol(token) + } + + #[ink(message)] + pub fn token_decimals(&self, token: TokenId) -> Result { + api::token_decimals(token) + } + + /// 3. Asset Management: + /// - create + /// - start_destroy + /// - set_metadata + /// - clear_metadata + /// - token_exists + + #[ink(message)] + pub fn create( + &mut self, + id: TokenId, + admin: AccountId, + min_balance: Balance, + ) -> Result<()> { + api::create(id, admin, min_balance)?; + self.env().emit_event(Created { id, creator: admin, admin }); + Ok(()) + } + + #[ink(message)] + pub fn start_destroy(&mut self, token: TokenId) -> Result<()> { + api::start_destroy(token)?; + self.env().emit_event(DestroyStarted { token }); + Ok(()) + } + + #[ink(message)] + pub fn set_metadata( + &mut self, + token: TokenId, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> Result<()> { + api::set_metadata(token, name.clone(), symbol.clone(), decimals)?; + self.env().emit_event(MetadataSet { token, name, symbol, decimals }); + Ok(()) + } + + #[ink(message)] + pub fn clear_metadata(&mut self, token: TokenId) -> Result<()> { + api::clear_metadata(token)?; + self.env().emit_event(MetadataCleared { token }); + Ok(()) + } + + #[ink(message)] + pub fn token_exists(&self, token: TokenId) -> Result { + api::token_exists(token) + } + + /// 4. PSP-22 Mintable & Burnable Interface: + /// - mint + /// - burn + + #[ink(message)] + pub fn mint(&mut self, token: TokenId, account: AccountId, amount: Balance) -> Result<()> { + api::mint(token, account, amount) + } + + #[ink(message)] + pub fn burn(&mut self, token: TokenId, account: AccountId, amount: Balance) -> Result<()> { + api::burn(token, account, amount) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[ink::test] + fn default_works() { + PopApiFungiblesExample::new(); + } + } +} diff --git a/pop-api/integration-tests/src/fungibles/mod.rs b/pop-api/integration-tests/src/fungibles/mod.rs new file mode 100644 index 00000000..d2e06285 --- /dev/null +++ b/pop-api/integration-tests/src/fungibles/mod.rs @@ -0,0 +1,554 @@ +use pop_api::fungibles::events::{ + Approval, Created, DestroyStarted, MetadataCleared, MetadataSet, Transfer, +}; +use pop_primitives::{ArithmeticError::*, Error, Error::*, TokenError::*, TokenId}; +use utils::*; + +use super::*; + +mod utils; + +const TOKEN_ID: TokenId = 1; +const CONTRACT: &str = "contracts/fungibles/target/ink/fungibles.wasm"; + +/// 1. PSP-22 Interface: +/// - total_supply +/// - balance_of +/// - allowance +/// - transfer +/// - transfer_from +/// - approve +/// - increase_allowance +/// - decrease_allowance + +#[test] +fn total_supply_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No tokens in circulation. + assert_eq!(total_supply(&addr, TOKEN_ID), Ok(Assets::total_supply(TOKEN_ID))); + assert_eq!(total_supply(&addr, TOKEN_ID), Ok(0)); + + // Tokens in circulation. + assets::create_and_mint_to(&addr, TOKEN_ID, &BOB, 100); + assert_eq!(total_supply(&addr, TOKEN_ID), Ok(Assets::total_supply(TOKEN_ID))); + assert_eq!(total_supply(&addr, TOKEN_ID), Ok(100)); + }); +} + +#[test] +fn balance_of_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No tokens in circulation. + assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(Assets::balance(TOKEN_ID, BOB))); + assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(0)); + + // Tokens in circulation. + assets::create_and_mint_to(&addr, TOKEN_ID, &BOB, 100); + assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(Assets::balance(TOKEN_ID, BOB))); + assert_eq!(balance_of(&addr, TOKEN_ID, BOB), Ok(100)); + }); +} + +#[test] +fn allowance_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No tokens in circulation. + assert_eq!( + allowance(&addr, TOKEN_ID, BOB, ALICE), + Ok(Assets::allowance(TOKEN_ID, &BOB, &ALICE)) + ); + assert_eq!(allowance(&addr, TOKEN_ID, BOB, ALICE), Ok(0)); + + // Tokens in circulation. + assets::create_mint_and_approve(&addr, TOKEN_ID, &BOB, 100, &ALICE, 50); + assert_eq!( + allowance(&addr, TOKEN_ID, BOB, ALICE), + Ok(Assets::allowance(TOKEN_ID, &BOB, &ALICE)) + ); + assert_eq!(allowance(&addr, TOKEN_ID, BOB, ALICE), Ok(50)); + }); +} + +#[test] +fn transfer_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!(transfer(&addr, 1, BOB, amount), Err(Module { index: 52, error: [3, 0] })); + // Create token with Alice as owner and mint `amount` to contract address. + let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&ALICE, token); + assert_eq!(transfer(&addr, token, BOB, amount), Err(Module { index: 52, error: [16, 0] })); + assets::thaw(&ALICE, token); + // Not enough balance. + assert_eq!( + transfer(&addr, token, BOB, amount + 1 * UNIT), + Err(Module { index: 52, error: [0, 0] }) + ); + // Not enough balance due to ED. + assert_eq!(transfer(&addr, token, BOB, amount), Err(Module { index: 52, error: [0, 0] })); + // Successful transfer. + let balance_before_transfer = Assets::balance(token, &BOB); + assert_ok!(transfer(&addr, token, BOB, amount / 2)); + let balance_after_transfer = Assets::balance(token, &BOB); + assert_eq!(balance_after_transfer, balance_before_transfer + amount / 2); + // Successfully emit event. + let from = account_id_from_slice(addr.as_ref()); + let to = account_id_from_slice(BOB.as_ref()); + let expected = Transfer { from: Some(from), to: Some(to), value: amount / 2 }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Transfer token to account that does not exist. + assert_eq!(transfer(&addr, token, FERDIE, amount / 4), Err(Token(CannotCreate))); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&ALICE, token); + assert_eq!( + transfer(&addr, token, BOB, amount / 4), + Err(Module { index: 52, error: [16, 0] }) + ); + }); +} + +#[test] +fn transfer_from_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!( + transfer_from(&addr, 1, ALICE, BOB, amount / 2), + Err(Module { index: 52, error: [3, 0] }), + ); + // Create token with Alice as owner and mint `amount` to contract address. + let token = assets::create_and_mint_to(&ALICE, 1, &ALICE, amount); + // Unapproved transfer. + assert_eq!( + transfer_from(&addr, token, ALICE, BOB, amount / 2), + Err(Module { index: 52, error: [10, 0] }) + ); + assert_ok!(Assets::approve_transfer( + RuntimeOrigin::signed(ALICE.into()), + token.into(), + addr.clone().into(), + amount + 1 * UNIT, + )); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&ALICE, token); + assert_eq!( + transfer_from(&addr, token, ALICE, BOB, amount), + Err(Module { index: 52, error: [16, 0] }), + ); + assets::thaw(&ALICE, token); + // Not enough balance. + assert_eq!( + transfer_from(&addr, token, ALICE, BOB, amount + 1 * UNIT), + Err(Module { index: 52, error: [0, 0] }), + ); + // Successful transfer. + let balance_before_transfer = Assets::balance(token, &BOB); + assert_ok!(transfer_from(&addr, token, ALICE, BOB, amount / 2)); + let balance_after_transfer = Assets::balance(token, &BOB); + assert_eq!(balance_after_transfer, balance_before_transfer + amount / 2); + // Successfully emit event. + let from = account_id_from_slice(ALICE.as_ref()); + let to = account_id_from_slice(BOB.as_ref()); + let expected = Transfer { from: Some(from), to: Some(to), value: amount / 2 }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + }); +} + +#[test] +fn approve_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, 0, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!(approve(&addr, 0, &BOB, amount), Err(Module { index: 52, error: [3, 0] })); + let token = assets::create_and_mint_to(&ALICE, 0, &addr, amount); + assert_eq!(approve(&addr, token, &BOB, amount), Err(ConsumerRemaining)); + let addr = instantiate(CONTRACT, INIT_VALUE, vec![1]); + // Create token with Alice as owner and mint `amount` to contract address. + let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&ALICE, token); + assert_eq!(approve(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); + assets::thaw(&ALICE, token); + // Successful approvals. + assert_eq!(0, Assets::allowance(token, &addr, &BOB)); + assert_ok!(approve(&addr, token, &BOB, amount)); + assert_eq!(Assets::allowance(token, &addr, &BOB), amount); + // Successfully emit event. + let owner = account_id_from_slice(addr.as_ref()); + let spender = account_id_from_slice(BOB.as_ref()); + let expected = Approval { owner, spender, value: amount }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Non-additive, sets new value. + assert_ok!(approve(&addr, token, &BOB, amount / 2)); + assert_eq!(Assets::allowance(token, &addr, &BOB), amount / 2); + // Successfully emit event. + let owner = account_id_from_slice(addr.as_ref()); + let spender = account_id_from_slice(BOB.as_ref()); + let expected = Approval { owner, spender, value: amount / 2 }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&ALICE, token); + assert_eq!(approve(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); + }); +} + +#[test] +fn increase_allowance_works() { + new_test_ext().execute_with(|| { + let amount: Balance = 100 * UNIT; + // Instantiate a contract without balance - test `ConsumerRemaining. + let addr = instantiate(CONTRACT, 0, vec![]); + // Token does not exist. + assert_eq!( + increase_allowance(&addr, 0, &BOB, amount), + Err(Module { index: 52, error: [3, 0] }) + ); + let token = assets::create_and_mint_to(&ALICE, 0, &addr, amount); + assert_eq!(increase_allowance(&addr, token, &BOB, amount), Err(ConsumerRemaining)); + + // Instantiate a contract with balance. + let addr = instantiate(CONTRACT, INIT_VALUE, vec![1]); + // Create token with Alice as owner and mint `amount` to contract address. + let token = assets::create_and_mint_to(&ALICE, 1, &addr, amount); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&ALICE, token); + assert_eq!( + increase_allowance(&addr, token, &BOB, amount), + Err(Module { index: 52, error: [16, 0] }) + ); + assets::thaw(&ALICE, token); + // Successful approvals: + assert_eq!(0, Assets::allowance(token, &addr, &BOB)); + assert_ok!(increase_allowance(&addr, token, &BOB, amount)); + assert_eq!(Assets::allowance(token, &addr, &BOB), amount); + // Additive. + assert_ok!(increase_allowance(&addr, token, &BOB, amount)); + assert_eq!(Assets::allowance(token, &addr, &BOB), amount * 2); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&ALICE, token); + assert_eq!( + increase_allowance(&addr, token, &BOB, amount), + Err(Module { index: 52, error: [16, 0] }) + ); + }); +} + +#[test] +fn decrease_allowance_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!( + decrease_allowance(&addr, 0, &BOB, amount), + Err(Module { index: 52, error: [3, 0] }), + ); + // Create token and mint `amount` to contract address, then approve Bob to spend `amount`. + let token = assets::create_mint_and_approve(&addr, 0, &addr, amount, &BOB, amount); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&addr, token); + assert_eq!( + decrease_allowance(&addr, token, &BOB, amount), + Err(Module { index: 52, error: [16, 0] }), + ); + assets::thaw(&addr, token); + // Successfully decrease allowance. + let allowance_before = Assets::allowance(token, &addr, &BOB); + assert_ok!(decrease_allowance(&addr, 0, &BOB, amount / 2 - 1 * UNIT)); + let allowance_after = Assets::allowance(token, &addr, &BOB); + assert_eq!(allowance_before - allowance_after, amount / 2 - 1 * UNIT); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&addr, token); + assert_eq!( + decrease_allowance(&addr, token, &BOB, amount), + Err(Module { index: 52, error: [16, 0] }), + ); + }); +} + +/// 2. PSP-22 Metadata Interface: +/// - token_name +/// - token_symbol +/// - token_decimals + +#[test] +fn token_metadata_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let name: Vec = vec![11, 12, 13]; + let symbol: Vec = vec![21, 22, 23]; + let decimals: u8 = 69; + + // Token does not exist. + assert_eq!(token_name(&addr, TOKEN_ID), Ok(Assets::name(TOKEN_ID))); + assert_eq!(token_name(&addr, TOKEN_ID), Ok(Vec::::new())); + assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(Assets::symbol(TOKEN_ID))); + assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(Vec::::new())); + assert_eq!(token_decimals(&addr, TOKEN_ID), Ok(Assets::decimals(TOKEN_ID))); + assert_eq!(token_decimals(&addr, TOKEN_ID), Ok(0)); + // Create Token. + assets::create_and_set_metadata(&addr, TOKEN_ID, name.clone(), symbol.clone(), decimals); + assert_eq!(token_name(&addr, TOKEN_ID), Ok(Assets::name(TOKEN_ID))); + assert_eq!(token_name(&addr, TOKEN_ID), Ok(name)); + assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(Assets::symbol(TOKEN_ID))); + assert_eq!(token_symbol(&addr, TOKEN_ID), Ok(symbol)); + assert_eq!(token_decimals(&addr, TOKEN_ID), Ok(Assets::decimals(TOKEN_ID))); + assert_eq!(token_decimals(&addr, TOKEN_ID), Ok(decimals)); + }); +} +/// 3. Management: +/// - create +/// - start_destroy +/// - set_metadata +/// - clear_metadata +/// - token_exists + +#[test] +fn create_works() { + new_test_ext().execute_with(|| { + // Instantiate a contract without balance for fees. + let addr = instantiate(CONTRACT, 0, vec![0]); + // No balance to pay for fees. + assert_eq!(create(&addr, TOKEN_ID, &addr, 1), Err(Module { index: 10, error: [2, 0] }),); + + // Instantiate a contract without balance for deposit. + let addr = instantiate(CONTRACT, 100, vec![1]); + // No balance to pay the deposit. + assert_eq!(create(&addr, TOKEN_ID, &addr, 1), Err(Module { index: 10, error: [2, 0] }),); + + // Instantiate a contract with enough balance. + let addr = instantiate(CONTRACT, INIT_VALUE, vec![2]); + assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); + // The minimal balance for a token must be non zero. + assert_eq!(create(&addr, TOKEN_ID, &BOB, 0), Err(Module { index: 52, error: [7, 0] }),); + // Create token successfully. + assert_ok!(create(&addr, TOKEN_ID, &BOB, 1)); + assert_eq!(Assets::owner(TOKEN_ID), Some(addr.clone())); + // Successfully emit event. + let admin = account_id_from_slice(BOB.as_ref()); + let expected = Created { id: TOKEN_ID, creator: admin, admin }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Token ID is already taken. + assert_eq!(create(&addr, TOKEN_ID, &BOB, 1), Err(Module { index: 52, error: [5, 0] }),); + }); +} + +// Testing a contract that creates a token in the constructor. +#[test] +fn instantiate_and_create_fungible_works() { + new_test_ext().execute_with(|| { + let contract = + "contracts/create_token_in_constructor/target/ink/create_token_in_constructor.wasm"; + // Token already exists. + assets::create(&ALICE, 0, 1); + assert_eq!( + instantiate_and_create_fungible(contract, 0, 1), + Err(Module { index: 52, error: [5, 0] }) + ); + // Successfully create a token when instantiating the contract. + let result_with_address = instantiate_and_create_fungible(contract, TOKEN_ID, 1); + let instantiator = result_with_address.clone().ok(); + assert_ok!(result_with_address); + assert_eq!(&Assets::owner(TOKEN_ID), &instantiator); + assert!(Assets::asset_exists(TOKEN_ID)); + // Successfully emit event. + let instantiator = account_id_from_slice(instantiator.unwrap().as_ref()); + let expected = + Created { id: TOKEN_ID, creator: instantiator.clone(), admin: instantiator }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + }); +} + +#[test] +fn start_destroy_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![2]); + + // Token does not exist. + assert_eq!(start_destroy(&addr, TOKEN_ID), Err(Module { index: 52, error: [3, 0] }),); + // Create tokens where contract is not the owner. + let token = assets::create(&ALICE, 0, 1); + // No Permission. + assert_eq!(start_destroy(&addr, token), Err(Module { index: 52, error: [2, 0] }),); + let token = assets::create(&addr, TOKEN_ID, 1); + assert_ok!(start_destroy(&addr, token)); + // Successfully emit event. + let expected = DestroyStarted { token: TOKEN_ID }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + }); +} + +#[test] +fn set_metadata_works() { + new_test_ext().execute_with(|| { + let name = vec![42]; + let symbol = vec![42]; + let decimals = 42u8; + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Token does not exist. + assert_eq!( + set_metadata(&addr, TOKEN_ID, vec![0], vec![0], 0u8), + Err(Module { index: 52, error: [3, 0] }), + ); + // Create token where contract is not the owner. + let token = assets::create(&ALICE, 0, 1); + // No Permission. + assert_eq!( + set_metadata(&addr, token, vec![0], vec![0], 0u8), + Err(Module { index: 52, error: [2, 0] }), + ); + let token = assets::create(&addr, TOKEN_ID, 1); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&addr, token); + assert_eq!( + set_metadata(&addr, TOKEN_ID, vec![0], vec![0], 0u8), + Err(Module { index: 52, error: [16, 0] }), + ); + assets::thaw(&addr, token); + // TODO: calling the below with a vector of length `100_000` errors in pallet contracts + // `OutputBufferTooSmall. Added to security analysis issue #131 to revisit. + // Set bad metadata - too large values. + assert_eq!( + set_metadata(&addr, TOKEN_ID, vec![0; 1000], vec![0; 1000], 0u8), + Err(Module { index: 52, error: [9, 0] }), + ); + // Set metadata successfully. + assert_ok!(set_metadata(&addr, TOKEN_ID, name.clone(), symbol.clone(), decimals)); + // Successfully emit event. + let expected = MetadataSet { token: TOKEN_ID, name, symbol, decimals }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&addr, token); + assert_eq!( + set_metadata(&addr, TOKEN_ID, vec![0], vec![0], 0), + Err(Module { index: 52, error: [16, 0] }), + ); + }); +} + +#[test] +fn clear_metadata_works() { + new_test_ext().execute_with(|| { + let name = vec![42]; + let symbol = vec![42]; + let decimals = 42u8; + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // Token does not exist. + assert_eq!(clear_metadata(&addr, 0), Err(Module { index: 52, error: [3, 0] }),); + // Create token where contract is not the owner. + let token = assets::create_and_set_metadata(&ALICE, 0, vec![0], vec![0], 0); + // No Permission. + assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [2, 0] }),); + let token = assets::create(&addr, TOKEN_ID, 1); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&addr, token); + assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [16, 0] }),); + assets::thaw(&addr, token); + // No metadata set. + assert_eq!(clear_metadata(&addr, token), Err(Module { index: 52, error: [3, 0] }),); + assets::set_metadata(&addr, token, name, symbol, decimals); + // Clear metadata successfully. + assert_ok!(clear_metadata(&addr, TOKEN_ID)); + // Successfully emit event. + let expected = MetadataCleared { token: TOKEN_ID }.encode(); + assert_eq!(last_contract_event(), expected.as_slice()); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&addr, token); + assert_eq!( + set_metadata(&addr, TOKEN_ID, vec![0], vec![0], decimals), + Err(Module { index: 52, error: [16, 0] }), + ); + }); +} + +#[test] +fn token_exists_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + + // No tokens in circulation. + assert_eq!(token_exists(&addr, TOKEN_ID), Ok(Assets::asset_exists(TOKEN_ID))); + + // Tokens in circulation. + assets::create(&addr, TOKEN_ID, 1); + assert_eq!(token_exists(&addr, TOKEN_ID), Ok(Assets::asset_exists(TOKEN_ID))); + }); +} + +#[test] +fn mint_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!(mint(&addr, 1, &BOB, amount), Err(Token(UnknownAsset))); + let token = assets::create(&ALICE, 1, 1); + // Minting can only be done by the owner. + assert_eq!(mint(&addr, token, &BOB, 1), Err(Module { index: 52, error: [2, 0] })); + let token = assets::create(&addr, 2, 2); + // Minimum balance of a token can not be zero. + assert_eq!(mint(&addr, token, &BOB, 1), Err(Token(BelowMinimum))); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&addr, token); + assert_eq!(mint(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); + assets::thaw(&addr, token); + // Successful mint. + let balance_before_mint = Assets::balance(token, &BOB); + assert_ok!(mint(&addr, token, &BOB, amount)); + let balance_after_mint = Assets::balance(token, &BOB); + assert_eq!(balance_after_mint, balance_before_mint + amount); + // Account can not hold more tokens than Balance::MAX. + assert_eq!(mint(&addr, token, &BOB, Balance::MAX,), Err(Arithmetic(Overflow))); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&addr, token); + assert_eq!(mint(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); + }); +} + +#[test] +fn burn_works() { + new_test_ext().execute_with(|| { + let addr = instantiate(CONTRACT, INIT_VALUE, vec![]); + let amount: Balance = 100 * UNIT; + + // Token does not exist. + assert_eq!(burn(&addr, 1, &BOB, amount), Err(Module { index: 52, error: [3, 0] })); + let token = assets::create(&ALICE, 1, 1); + // Bob has no tokens and therefore doesn't exist. + assert_eq!(burn(&addr, token, &BOB, 1), Err(Module { index: 52, error: [1, 0] })); + // Burning can only be done by the manager. + assets::mint(&ALICE, token, &BOB, amount); + assert_eq!(burn(&addr, token, &BOB, 1), Err(Module { index: 52, error: [2, 0] })); + let token = assets::create_and_mint_to(&addr, 2, &BOB, amount); + // Token is not live, i.e. frozen or being destroyed. + assets::freeze(&addr, token); + assert_eq!(burn(&addr, token, &BOB, amount), Err(Module { index: 52, error: [16, 0] })); + assets::thaw(&addr, token); + // Successful mint. + let balance_before_burn = Assets::balance(token, &BOB); + assert_ok!(burn(&addr, token, &BOB, amount)); + let balance_after_burn = Assets::balance(token, &BOB); + assert_eq!(balance_after_burn, balance_before_burn - amount); + // Token is not live, i.e. frozen or being destroyed. + assets::start_destroy(&addr, token); + assert_eq!(burn(&addr, token, &BOB, amount), Err(Module { index: 52, error: [17, 0] })); + }); +} diff --git a/pop-api/integration-tests/src/fungibles/utils.rs b/pop-api/integration-tests/src/fungibles/utils.rs new file mode 100644 index 00000000..85fb3007 --- /dev/null +++ b/pop-api/integration-tests/src/fungibles/utils.rs @@ -0,0 +1,361 @@ +use super::*; + +fn do_bare_call(function: &str, addr: &AccountId32, params: Vec) -> ExecReturnValue { + let function = function_selector(function); + let params = [function, params].concat(); + bare_call(addr.clone(), params, 0).expect("should work") +} + +// TODO - issue #263 - why result.data[1..] +pub(super) fn decoded(result: ExecReturnValue) -> Result { + ::decode(&mut &result.data[1..]).map_err(|_| result) +} + +pub(super) fn total_supply(addr: &AccountId32, token_id: TokenId) -> Result { + let result = do_bare_call("total_supply", addr, token_id.encode()); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn balance_of( + addr: &AccountId32, + token_id: TokenId, + owner: AccountId32, +) -> Result { + let params = [token_id.encode(), owner.encode()].concat(); + let result = do_bare_call("balance_of", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn allowance( + addr: &AccountId32, + token_id: TokenId, + owner: AccountId32, + spender: AccountId32, +) -> Result { + let params = [token_id.encode(), owner.encode(), spender.encode()].concat(); + let result = do_bare_call("allowance", &addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn token_name(addr: &AccountId32, token_id: TokenId) -> Result, Error> { + let result = do_bare_call("token_name", addr, token_id.encode()); + decoded::, Error>>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn token_symbol(addr: &AccountId32, token_id: TokenId) -> Result, Error> { + let result = do_bare_call("token_symbol", addr, token_id.encode()); + decoded::, Error>>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn token_decimals(addr: &AccountId32, token_id: TokenId) -> Result { + let result = do_bare_call("token_decimals", addr, token_id.encode()); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn token_exists(addr: &AccountId32, token_id: TokenId) -> Result { + let result = do_bare_call("token_exists", addr, token_id.encode()); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn transfer( + addr: &AccountId32, + token_id: TokenId, + to: AccountId32, + value: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), to.encode(), value.encode()].concat(); + let result = do_bare_call("transfer", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn transfer_from( + addr: &AccountId32, + token_id: TokenId, + from: AccountId32, + to: AccountId32, + value: Balance, +) -> Result<(), Error> { + let data: Vec = vec![]; + let params = + [token_id.encode(), from.encode(), to.encode(), value.encode(), data.encode()].concat(); + let result = do_bare_call("transfer_from", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn approve( + addr: &AccountId32, + token_id: TokenId, + spender: &AccountId32, + value: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), spender.encode(), value.encode()].concat(); + let result = do_bare_call("approve", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn increase_allowance( + addr: &AccountId32, + token_id: TokenId, + spender: &AccountId32, + value: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), spender.encode(), value.encode()].concat(); + let result = do_bare_call("increase_allowance", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn decrease_allowance( + addr: &AccountId32, + token_id: TokenId, + spender: &AccountId32, + value: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), spender.encode(), value.encode()].concat(); + let result = do_bare_call("decrease_allowance", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn create( + addr: &AccountId32, + token_id: TokenId, + admin: &AccountId32, + min_balance: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), admin.encode(), min_balance.encode()].concat(); + let result = do_bare_call("create", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn start_destroy(addr: &AccountId32, token_id: TokenId) -> Result<(), Error> { + let result = do_bare_call("start_destroy", addr, token_id.encode()); + match decoded::>(result) { + Ok(x) => x, + Err(result) => panic!("Contract reverted: {:?}", result), + } +} + +pub(super) fn set_metadata( + addr: &AccountId32, + token_id: TokenId, + name: Vec, + symbol: Vec, + decimals: u8, +) -> Result<(), Error> { + let params = [token_id.encode(), name.encode(), symbol.encode(), decimals.encode()].concat(); + let result = do_bare_call("set_metadata", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn clear_metadata(addr: &AccountId32, token_id: TokenId) -> Result<(), Error> { + let result = do_bare_call("clear_metadata", addr, token_id.encode()); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn mint( + addr: &AccountId32, + token_id: TokenId, + account: &AccountId32, + amount: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), account.encode(), amount.encode()].concat(); + let result = do_bare_call("mint", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +pub(super) fn burn( + addr: &AccountId32, + token_id: TokenId, + account: &AccountId32, + amount: Balance, +) -> Result<(), Error> { + let params = [token_id.encode(), account.encode(), amount.encode()].concat(); + let result = do_bare_call("burn", addr, params); + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) +} + +// Helper functions for interacting with pallet-assets. +pub(super) mod assets { + use super::*; + + type AssetId = TokenId; + + pub(crate) fn create(owner: &AccountId32, asset_id: AssetId, min_balance: Balance) -> AssetId { + assert_ok!(Assets::create( + RuntimeOrigin::signed(owner.clone()), + asset_id.into(), + owner.clone().into(), + min_balance + )); + asset_id + } + + pub(crate) fn mint( + owner: &AccountId32, + asset_id: AssetId, + to: &AccountId32, + value: Balance, + ) -> AssetId { + assert_ok!(Assets::mint( + RuntimeOrigin::signed(owner.clone()), + asset_id.into(), + to.clone().into(), + value + )); + asset_id + } + + pub(crate) fn create_and_mint_to( + owner: &AccountId32, + asset_id: AssetId, + to: &AccountId32, + value: Balance, + ) -> AssetId { + create(owner, asset_id, 1); + mint(owner, asset_id, to, value) + } + + // Create an asset, mints to, and approves spender. + pub(crate) fn create_mint_and_approve( + owner: &AccountId32, + asset_id: AssetId, + to: &AccountId32, + mint: Balance, + spender: &AccountId32, + approve: Balance, + ) -> AssetId { + create_and_mint_to(owner, asset_id, to, mint); + assert_ok!(Assets::approve_transfer( + RuntimeOrigin::signed(to.clone().into()), + asset_id.into(), + spender.clone().into(), + approve, + )); + asset_id + } + + // Freeze an asset. + pub(crate) fn freeze(owner: &AccountId32, asset_id: AssetId) { + assert_ok!(Assets::freeze_asset( + RuntimeOrigin::signed(owner.clone().into()), + asset_id.into() + )); + } + + // Thaw an asset. + pub(crate) fn thaw(owner: &AccountId32, asset_id: AssetId) { + assert_ok!(Assets::thaw_asset( + RuntimeOrigin::signed(owner.clone().into()), + asset_id.into() + )); + } + + // Start destroying an asset. + pub(crate) fn start_destroy(owner: &AccountId32, asset_id: AssetId) { + assert_ok!(Assets::start_destroy( + RuntimeOrigin::signed(owner.clone().into()), + asset_id.into() + )); + } + + // Create an asset and set metadata. + pub(crate) fn create_and_set_metadata( + owner: &AccountId32, + asset_id: AssetId, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> AssetId { + assert_ok!(Assets::create( + RuntimeOrigin::signed(owner.clone()), + asset_id.into(), + owner.clone().into(), + 100 + )); + set_metadata(owner, asset_id, name, symbol, decimals); + asset_id + } + + // Set metadata of an asset. + pub(crate) fn set_metadata( + owner: &AccountId32, + asset_id: AssetId, + name: Vec, + symbol: Vec, + decimals: u8, + ) { + assert_ok!(Assets::set_metadata( + RuntimeOrigin::signed(owner.clone().into()), + asset_id.into(), + name, + symbol, + decimals + )); + } +} + +pub(super) fn instantiate_and_create_fungible( + contract: &str, + token_id: TokenId, + min_balance: Balance, +) -> Result { + let function = function_selector("new"); + let input = [function, token_id.encode(), min_balance.encode()].concat(); + let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); + let result = Contracts::bare_instantiate( + ALICE, + INIT_VALUE, + GAS_LIMIT, + None, + Code::Upload(wasm_binary), + input, + vec![], + DEBUG_OUTPUT, + CollectEvents::Skip, + ) + .result + .expect("should work"); + let address = result.account_id; + let result = result.result; + decoded::>(result.clone()) + .unwrap_or_else(|_| panic!("Contract reverted: {:?}", result)) + .map(|_| address) +} + +/// Get the last event from pallet contracts. +pub(super) fn last_contract_event() -> Vec { + let events = System::read_events_for_pallet::>(); + let contract_events = events + .iter() + .filter_map(|event| match event { + pallet_contracts::Event::::ContractEmitted { data, .. } => + Some(data.as_slice()), + _ => None, + }) + .collect::>(); + contract_events.last().unwrap().to_vec() +} + +/// Decodes a byte slice into an `AccountId` as defined in `primitives`. +/// +/// This is used to resolve type mismatches between the `AccountId` in the integration tests and the +/// contract environment. +pub fn account_id_from_slice(s: &[u8; 32]) -> pop_api::primitives::AccountId { + pop_api::primitives::AccountId::decode(&mut &s[..]).expect("Should be decoded to AccountId") +} diff --git a/pop-api/integration-tests/src/lib.rs b/pop-api/integration-tests/src/lib.rs new file mode 100644 index 00000000..9e6e20fc --- /dev/null +++ b/pop-api/integration-tests/src/lib.rs @@ -0,0 +1,90 @@ +#![cfg(test)] + +use frame_support::{ + assert_ok, + traits::fungibles::{ + approvals::Inspect as _, metadata::Inspect as _, roles::Inspect as _, Inspect, + }, + weights::Weight, +}; +use pallet_contracts::{Code, CollectEvents, Determinism, ExecReturnValue}; +use pop_runtime_devnet::{Assets, Contracts, Runtime, RuntimeOrigin, System, UNIT}; +use scale::{Decode, Encode}; +use sp_runtime::{AccountId32, BuildStorage, DispatchError}; + +mod fungibles; + +type Balance = u128; + +const ALICE: AccountId32 = AccountId32::new([1_u8; 32]); +const BOB: AccountId32 = AccountId32::new([2_u8; 32]); +const DEBUG_OUTPUT: pallet_contracts::DebugInfo = pallet_contracts::DebugInfo::UnsafeDebug; +const FERDIE: AccountId32 = AccountId32::new([3_u8; 32]); +const GAS_LIMIT: Weight = Weight::from_parts(100_000_000_000, 3 * 1024 * 1024); +const INIT_AMOUNT: Balance = 100_000_000 * UNIT; +const INIT_VALUE: Balance = 100 * UNIT; + +fn new_test_ext() -> sp_io::TestExternalities { + let _ = env_logger::try_init(); + + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + // FERDIE has no initial balance. + balances: vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT)], + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +fn function_selector(name: &str) -> Vec { + let hash = sp_io::hashing::blake2_256(name.as_bytes()); + [hash[0..4].to_vec()].concat() +} + +fn bare_call( + addr: AccountId32, + input: Vec, + value: u128, +) -> Result { + let result = Contracts::bare_call( + ALICE, + addr.into(), + value.into(), + GAS_LIMIT, + None, + input, + DEBUG_OUTPUT, + CollectEvents::Skip, + Determinism::Enforced, + ); + log::info!("contract exec result={result:?}"); + result.result +} + +// Deploy, instantiate and return contract address. +fn instantiate(contract: &str, init_value: u128, salt: Vec) -> AccountId32 { + let wasm_binary = std::fs::read(contract).expect("could not read .wasm file"); + + let result = Contracts::bare_instantiate( + ALICE, + init_value, + GAS_LIMIT, + None, + Code::Upload(wasm_binary), + function_selector("new"), + salt, + DEBUG_OUTPUT, + CollectEvents::Skip, + ) + .result + .unwrap(); + assert!(!result.result.did_revert(), "deploying contract reverted {:?}", result); + result.account_id +} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index e4590c89..546106e5 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -1,101 +1,90 @@ +//! The `pop-api` crate provides an API for smart contracts to interact with the Pop Network +//! runtime. +//! +//! This crate abstracts away complexities to deliver a streamlined developer experience while +//! supporting multiple API versions to ensure backward compatibility. It is designed with a focus +//! on stability, future-proofing, and storage efficiency, allowing developers to easily integrate +//! powerful runtime features into their contracts without unnecessary overhead. + #![cfg_attr(not(feature = "std"), no_std, no_main)] +use constants::DECODING_FAILED; +use ink::env::chain_extension::{ChainExtensionMethod, FromStatusCode}; +pub use v0::*; + +/// Module providing primitives types. pub mod primitives; +/// The first version of the API. pub mod v0; -use crate::PopApiError::{Balances, Nfts, UnknownStatusCode}; -use ink::{prelude::vec::Vec, ChainExtensionInstance}; -use primitives::{cross_chain::*, storage_keys::*}; -pub use sp_runtime::{BoundedVec, MultiAddress, MultiSignature}; -use v0::RuntimeCall; -pub use v0::{balances, cross_chain, nfts, relay_chain_block_number, state}; - -type AccountId = ::AccountId; -type Balance = ::Balance; -type BlockNumber = ::BlockNumber; -type StringLimit = u32; -type MaxTips = u32; - -pub type Result = core::result::Result; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum PopApiError { - UnknownStatusCode(u32), - DecodingFailed, - SystemCallFiltered, - Balances(balances::Error), - Nfts(nfts::Error), - Xcm(cross_chain::Error), +type ChainExtensionMethodApi = ChainExtensionMethod<(), (), (), false>; +/// The result type used by the API, with the `StatusCode` as the error type. +pub type Result = core::result::Result; + +/// Represents a status code returned by the runtime. +/// +/// `StatusCode` encapsulates a `u32` value that indicates the success or failure of a runtime call +/// via Pop API. +/// +/// A `StatusCode` of `0` indicates success, while any other value represents an +/// error. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct StatusCode(pub u32); + +impl From for StatusCode { + /// Converts a `u32` into a `StatusCode`. + fn from(value: u32) -> Self { + StatusCode(value) + } } -impl ink::env::chain_extension::FromStatusCode for PopApiError { - fn from_status_code(status_code: u32) -> core::result::Result<(), Self> { +impl FromStatusCode for StatusCode { + /// Converts a `u32` status code to a `Result`. + /// + /// `Ok(())` if the status code is `0` and `Err(StatusCode(status_code))` for any other status + /// code. + fn from_status_code(status_code: u32) -> Result<()> { match status_code { 0 => Ok(()), - // CallFiltered originates from `frame_system` with pallet-index 0. The CallFiltered error is at index 5 - 5 => Err(PopApiError::SystemCallFiltered), - 10_000..=10_999 => Err(Balances((status_code - 10_000).try_into()?)), - 50_000..=50_999 => Err(Nfts((status_code - 50_000).try_into()?)), - _ => Err(UnknownStatusCode(status_code)), + _ => Err(StatusCode(status_code)), } } } -impl From for PopApiError { - fn from(_: scale::Error) -> Self { - panic!("encountered unexpected invalid SCALE encoding") +impl From for StatusCode { + /// Converts a scale decoding error into a `StatusCode` indicating a decoding failure. + fn from(_: ink::scale::Error) -> Self { + StatusCode(DECODING_FAILED) } } -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum Environment {} - -impl ink::env::Environment for Environment { - const MAX_EVENT_TOPICS: usize = - ::MAX_EVENT_TOPICS; - - type AccountId = ::AccountId; - type Balance = ::Balance; - type Hash = ::Hash; - type BlockNumber = ::BlockNumber; - type Timestamp = ::Timestamp; - - type ChainExtension = PopApi; -} - -#[ink::chain_extension(extension = 909)] -pub trait PopApi { - type ErrorCode = PopApiError; - - #[ink(function = 0)] - #[allow(private_interfaces)] - fn dispatch(call: RuntimeCall) -> Result<()>; +mod constants { + // Error. + pub(crate) const DECODING_FAILED: u32 = 255; - #[ink(function = 1)] - #[allow(private_interfaces)] - fn read_state(key: RuntimeStateKeys) -> Result>; - - #[ink(function = 2)] - #[allow(private_interfaces)] - fn send_xcm(xcm: CrossChainMessage) -> Result<()>; -} - -fn dispatch(call: RuntimeCall) -> Result<()> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .dispatch(call) -} + // Function IDs. + pub(crate) const DISPATCH: u8 = 0; + pub(crate) const READ_STATE: u8 = 1; -fn read_state(key: RuntimeStateKeys) -> Result> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .read_state(key) + // Modules. + pub(crate) const ASSETS: u8 = 52; + pub(crate) const BALANCES: u8 = 10; + pub(crate) const FUNGIBLES: u8 = 150; } -fn send_xcm(xcm: CrossChainMessage) -> Result<()> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .send_xcm(xcm) +// Helper method to build a dispatch call or a call to read state. +// +// Parameters: +// - 'function': The ID of the function. +// - 'version': The version of the chain extension. +// - 'module': The index of the runtime module. +// - 'dispatchable': The index of the module dispatchable functions. +fn build_extension_method( + function: u8, + version: u8, + module: u8, + dispatchable: u8, +) -> ChainExtensionMethodApi { + ChainExtensionMethod::build(u32::from_le_bytes([function, version, module, dispatchable])) } diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index e8098f69..2fcb8a95 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -1,2 +1,6 @@ +use ink::env::{DefaultEnvironment, Environment}; pub use pop_primitives::*; -pub use sp_runtime::{BoundedVec, MultiAddress}; + +// Public due to integration tests crate. +pub type AccountId = ::AccountId; +pub(crate) type Balance = ::Balance; diff --git a/pop-api/src/v0/balances.rs b/pop-api/src/v0/balances.rs deleted file mode 100644 index bf029178..00000000 --- a/pop-api/src/v0/balances.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::{ - dispatch, primitives::MultiAddress, v0::RuntimeCall, AccountId, PopApiError, - PopApiError::UnknownStatusCode, -}; - -type Result = core::result::Result; - -pub fn transfer_keep_alive( - dest: impl Into>, - value: u128, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Balances(BalancesCall::TransferKeepAlive { - dest: dest.into(), - value, - }))?) -} - -#[derive(scale::Encode)] -#[allow(dead_code)] -pub(crate) enum BalancesCall { - #[codec(index = 3)] - TransferKeepAlive { - dest: MultiAddress, - #[codec(compact)] - value: u128, - }, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum Error { - /// Vesting balance too high to send value. - VestingBalance, - /// Account liquidity restrictions prevent withdrawal. - LiquidityRestrictions, - /// Balance too low to send value. - InsufficientBalance, - /// Value too low to create account due to existential deposit. - ExistentialDeposit, - /// Transfer/payment would kill account. - Expendability, - /// A vesting schedule already exists for this account. - ExistingVestingSchedule, - /// Beneficiary account must pre-exist. - DeadAccount, - /// Number of named reserves exceed `MaxReserves`. - TooManyReserves, - /// Number of holds exceed `VariantCountOf`. - TooManyHolds, - /// Number of freezes exceed `MaxFreezes`. - TooManyFreezes, - /// The issuance cannot be modified since it is already deactivated. - IssuanceDeactivated, - /// The delta cannot be zero. - DeltaZero, -} - -impl TryFrom for Error { - type Error = PopApiError; - - fn try_from(status_code: u32) -> core::result::Result { - use Error::*; - match status_code { - 0 => Ok(VestingBalance), - 1 => Ok(LiquidityRestrictions), - 2 => Ok(InsufficientBalance), - 3 => Ok(ExistentialDeposit), - 4 => Ok(Expendability), - 5 => Ok(ExistingVestingSchedule), - 6 => Ok(DeadAccount), - 7 => Ok(TooManyReserves), - 8 => Ok(TooManyHolds), - 9 => Ok(TooManyFreezes), - 10 => Ok(IssuanceDeactivated), - 11 => Ok(DeltaZero), - _ => Err(UnknownStatusCode(status_code)), - } - } -} - -impl From for Error { - fn from(error: PopApiError) -> Self { - match error { - PopApiError::Balances(e) => e, - _ => panic!("expected balances error"), - } - } -} diff --git a/pop-api/src/v0/cross_chain/coretime.rs b/pop-api/src/v0/cross_chain/coretime.rs deleted file mode 100644 index 0039ed20..00000000 --- a/pop-api/src/v0/cross_chain/coretime.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::{ - primitives::cross_chain::{CrossChainMessage, OnDemand, RelayChainMessage}, - send_xcm, -}; - -/// Send a cross-chain message to place a sport order for instantaneous coretime. -pub fn place_spot_order(max_amount: u128, para_id: u32) -> crate::cross_chain::Result<()> { - Ok(send_xcm(CrossChainMessage::Relay(RelayChainMessage::OnDemand( - OnDemand::PlaceOrderKeepAlive { max_amount, para_id }, - )))?) -} diff --git a/pop-api/src/v0/cross_chain/mod.rs b/pop-api/src/v0/cross_chain/mod.rs deleted file mode 100644 index 6732c119..00000000 --- a/pop-api/src/v0/cross_chain/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub mod coretime; - -use crate::{PopApiError::UnknownStatusCode, *}; - -type Result = core::result::Result; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum Error { - /// The desired destination was unreachable, generally because there is a no way of routing - /// to it. - Unreachable, - /// There was some other issue (i.e. not to do with routing) in sending the message. - /// Perhaps a lack of space for buffering the message. - SendFailure, - /// The message execution fails the filter. - Filtered, - /// The message's weight could not be determined. - UnweighableMessage, - /// The destination `Location` provided cannot be inverted. - DestinationNotInvertible, - /// The assets to be sent are empty. - Empty, - /// Could not re-anchor the assets to declare the fees for the destination chain. - CannotReanchor, - /// Too many assets have been attempted for transfer. - TooManyAssets, - /// Origin is invalid for sending. - InvalidOrigin, - /// The version of the `Versioned` value used is not able to be interpreted. - BadVersion, - /// The given location could not be used (e.g. because it cannot be expressed in the - /// desired version of XCM). - BadLocation, - /// The referenced subscription could not be found. - NoSubscription, - /// The location is invalid since it already has a subscription from us. - AlreadySubscribed, - /// Could not check-out the assets for teleportation to the destination chain. - CannotCheckOutTeleport, - /// The owner does not own (all) of the asset that they wish to do the operation on. - LowBalance, - /// The asset owner has too many locks on the asset. - TooManyLocks, - /// The given account is not an identifiable sovereign account for any location. - AccountNotSovereign, - /// The operation required fees to be paid which the initiator could not meet. - FeesNotMet, - /// A remote lock with the corresponding data could not be found. - LockNotFound, - /// The unlock operation cannot succeed because there are still consumers of the lock. - InUse, - /// Invalid non-concrete asset. - InvalidAssetNotConcrete, - /// Invalid asset, reserve chain could not be determined for it. - InvalidAssetUnknownReserve, - /// Invalid asset, do not support remote asset reserves with different fees reserves. - InvalidAssetUnsupportedReserve, - /// Too many assets with different reserve locations have been attempted for transfer. - TooManyReserves, - /// Local XCM execution incomplete. - LocalExecutionIncomplete, -} - -impl TryFrom for Error { - type Error = PopApiError; - - fn try_from(status_code: u32) -> core::result::Result { - use Error::*; - match status_code { - 0 => Ok(Unreachable), - 1 => Ok(SendFailure), - 2 => Ok(Filtered), - 3 => Ok(UnweighableMessage), - 4 => Ok(DestinationNotInvertible), - 5 => Ok(Empty), - 6 => Ok(CannotReanchor), - 7 => Ok(TooManyAssets), - 8 => Ok(InvalidOrigin), - 9 => Ok(BadVersion), - 10 => Ok(BadLocation), - 11 => Ok(NoSubscription), - 12 => Ok(AlreadySubscribed), - 13 => Ok(CannotCheckOutTeleport), - 14 => Ok(LowBalance), - 15 => Ok(TooManyLocks), - 16 => Ok(AccountNotSovereign), - 17 => Ok(FeesNotMet), - 18 => Ok(LockNotFound), - 19 => Ok(InUse), - 20 => Ok(InvalidAssetNotConcrete), - 21 => Ok(InvalidAssetUnknownReserve), - 22 => Ok(InvalidAssetUnsupportedReserve), - 23 => Ok(TooManyReserves), - _ => Err(UnknownStatusCode(status_code)), - } - } -} - -impl From for Error { - fn from(error: PopApiError) -> Self { - match error { - PopApiError::Xcm(e) => e, - _ => panic!("expected xcm error"), - } - } -} diff --git a/pop-api/src/v0/fungibles.rs b/pop-api/src/v0/fungibles.rs new file mode 100644 index 00000000..99c1261a --- /dev/null +++ b/pop-api/src/v0/fungibles.rs @@ -0,0 +1,588 @@ +//! The `fungibles` module provides an API for interacting and managing fungible tokens. +//! +//! The API includes the following interfaces: +//! 1. PSP-22 +//! 2. PSP-22 Metadata +//! 3. Management +//! 4. PSP-22 Mintable & Burnable + +use constants::*; +use ink::prelude::vec::Vec; +pub use management::*; +pub use metadata::*; + +use crate::{ + constants::{ASSETS, BALANCES, FUNGIBLES}, + primitives::{AccountId, Balance, TokenId}, + ChainExtensionMethodApi, Result, StatusCode, +}; + +// Helper method to build a dispatch call. +// +// Parameters: +// - 'dispatchable': The index of the dispatchable function within the module. +fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { + crate::v0::build_dispatch(FUNGIBLES, dispatchable) +} + +// Helper method to build a call to read state. +// +// Parameters: +// - 'state_query': The index of the runtime state query. +fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { + crate::v0::build_read_state(FUNGIBLES, state_query) +} + +mod constants { + /// 1. PSP-22 Interface: + pub(super) const TOTAL_SUPPLY: u8 = 0; + pub(super) const BALANCE_OF: u8 = 1; + pub(super) const ALLOWANCE: u8 = 2; + pub(super) const TRANSFER: u8 = 3; + pub(super) const TRANSFER_FROM: u8 = 4; + pub(super) const APPROVE: u8 = 5; + pub(super) const INCREASE_ALLOWANCE: u8 = 6; + pub(super) const DECREASE_ALLOWANCE: u8 = 7; + + /// 2. PSP-22 Metadata Interface: + pub(super) const TOKEN_NAME: u8 = 8; + pub(super) const TOKEN_SYMBOL: u8 = 9; + pub(super) const TOKEN_DECIMALS: u8 = 10; + + /// 3. Asset Management: + pub(super) const CREATE: u8 = 11; + pub(super) const START_DESTROY: u8 = 12; + pub(super) const SET_METADATA: u8 = 16; + pub(super) const CLEAR_METADATA: u8 = 17; + pub(super) const TOKEN_EXISTS: u8 = 18; + + /// 4. PSP-22 Mintable & Burnable interface: + pub(super) const MINT: u8 = 19; + pub(super) const BURN: u8 = 20; +} + +/// A set of events for use in smart contracts interacting with the fungibles API. +/// +/// The `Transfer` and `Approval` events conform to the PSP-22 standard. The other events +/// (`Create`, `StartDestroy`, `SetMetadata`, `ClearMetadata`) are provided for convenience. +/// +/// These events are not emitted by the API itself but can be used in your contracts to +/// track token operations. Be mindful of the costs associated with emitting events. +/// +/// For more details, refer to [ink! events](https://use.ink/basics/events). +pub mod events { + use super::*; + + /// Event emitted when allowance by `owner` to `spender` changes. + #[ink::event] + pub struct Approval { + /// The owner providing the allowance. + #[ink(topic)] + pub owner: AccountId, + /// The beneficiary of the allowance. + #[ink(topic)] + pub spender: AccountId, + /// The new allowance amount. + pub value: u128, + } + + /// Event emitted when transfer of tokens occurs. + #[ink::event] + pub struct Transfer { + /// The source of the transfer. `None` when minting. + #[ink(topic)] + pub from: Option, + /// The recipient of the transfer. `None` when burning. + #[ink(topic)] + pub to: Option, + /// The amount transferred (or minted/burned). + pub value: u128, + } + + /// Event emitted when a token is created. + #[ink::event] + pub struct Created { + /// The token identifier. + #[ink(topic)] + pub id: TokenId, + /// The creator of the token. + #[ink(topic)] + pub creator: AccountId, + /// The administrator of the token. + #[ink(topic)] + pub admin: AccountId, + } + + /// Event emitted when a token is in the process of being destroyed. + #[ink::event] + pub struct DestroyStarted { + /// The token. + #[ink(topic)] + pub token: TokenId, + } + + /// Event emitted when new metadata is set for a token. + #[ink::event] + pub struct MetadataSet { + /// The token. + #[ink(topic)] + pub token: TokenId, + /// The name of the token. + #[ink(topic)] + pub name: Vec, + /// The symbol of the token. + #[ink(topic)] + pub symbol: Vec, + /// The decimals of the token. + pub decimals: u8, + } + + /// Event emitted when metadata is cleared for a token. + #[ink::event] + pub struct MetadataCleared { + /// The token. + #[ink(topic)] + pub token: TokenId, + } +} + +/// Returns the total token supply for a specified token. +/// +/// # Parameters +/// - `token` - The token. +#[inline] +pub fn total_supply(token: TokenId) -> Result { + build_read_state(TOTAL_SUPPLY) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(token)) +} + +/// Returns the account balance for a specified `token` and `owner`. Returns `0` if +/// the account is non-existent. +/// +/// # Parameters +/// - `token` - The token. +/// - `owner` - The account whose balance is being queried. +#[inline] +pub fn balance_of(token: TokenId, owner: AccountId) -> Result { + build_read_state(BALANCE_OF) + .input::<(TokenId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(token, owner)) +} + +/// Returns the allowance for a `spender` approved by an `owner`, for a specified `token`. Returns +/// `0` if no allowance has been set. +/// +/// # Parameters +/// - `token` - The token. +/// - `owner` - The account that owns the tokens. +/// - `spender` - The account that is allowed to spend the tokens. +#[inline] +pub fn allowance(token: TokenId, owner: AccountId, spender: AccountId) -> Result { + build_read_state(ALLOWANCE) + .input::<(TokenId, AccountId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(token, owner, spender)) +} + +/// Transfers `value` amount of tokens from the caller's account to account `to`. +/// +/// # Parameters +/// - `token` - The token to transfer. +/// - `to` - The recipient account. +/// - `value` - The number of tokens to transfer. +#[inline] +pub fn transfer(token: TokenId, to: AccountId, value: Balance) -> Result<()> { + build_dispatch(TRANSFER) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, to, value)) +} + +/// Transfers `value` amount tokens on behalf of `from` to account `to`. +/// +/// # Parameters +/// - `token` - The token to transfer. +/// - `from` - The account from which the token balance will be withdrawn. +/// - `to` - The recipient account. +/// - `value` - The number of tokens to transfer. +#[inline] +pub fn transfer_from(token: TokenId, from: AccountId, to: AccountId, value: Balance) -> Result<()> { + build_dispatch(TRANSFER_FROM) + .input::<(TokenId, AccountId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, from, to, value)) +} + +/// Approves `spender` to spend `value` amount of tokens on behalf of the caller. +/// +/// # Parameters +/// - `token` - The token to approve. +/// - `spender` - The account that is allowed to spend the tokens. +/// - `value` - The number of tokens to approve. +#[inline] +pub fn approve(token: TokenId, spender: AccountId, value: Balance) -> Result<()> { + build_dispatch(APPROVE) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, spender, value)) +} + +/// Increases the allowance of `spender` by `value` amount of tokens. +/// +/// # Parameters +/// - `token` - The token to have an allowance increased. +/// - `spender` - The account that is allowed to spend the tokens. +/// - `value` - The number of tokens to increase the allowance by. +#[inline] +pub fn increase_allowance(token: TokenId, spender: AccountId, value: Balance) -> Result<()> { + build_dispatch(INCREASE_ALLOWANCE) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, spender, value)) +} + +/// Decreases the allowance of `spender` by `value` amount of tokens. +/// +/// # Parameters +/// - `token` - The token to have an allowance decreased. +/// - `spender` - The account that is allowed to spend the tokens. +/// - `value` - The number of tokens to decrease the allowance by. +#[inline] +pub fn decrease_allowance(token: TokenId, spender: AccountId, value: Balance) -> Result<()> { + build_dispatch(DECREASE_ALLOWANCE) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, spender, value)) +} + +/// Creates `value` amount of tokens and assigns them to `account`, increasing the total supply. +/// +/// # Parameters +/// - `token` - The token to mint. +/// - `account` - The account to be credited with the created tokens. +/// - `value` - The number of tokens to mint. +#[inline] +pub fn mint(token: TokenId, account: AccountId, value: Balance) -> Result<()> { + build_dispatch(MINT) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, account, value)) +} + +/// Destroys `value` amount of tokens from `account`, reducing the total supply. +/// +/// # Parameters +/// - `token` - the token to burn. +/// - `account` - The account from which the tokens will be destroyed. +/// - `value` - The number of tokens to destroy. +#[inline] +pub fn burn(token: TokenId, account: AccountId, value: Balance) -> Result<()> { + build_dispatch(BURN) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(token, account, value)) +} + +/// The PSP-22 compliant interface for querying metadata. +pub mod metadata { + use super::*; + + /// Returns the name of the specified token. + /// + /// # Parameters + /// - `token` - The token. + #[inline] + pub fn token_name(token: TokenId) -> Result> { + build_read_state(TOKEN_NAME) + .input::() + .output::>, true>() + .handle_error_code::() + .call(&(token)) + } + + /// Returns the symbol for the specified token. + /// + /// # Parameters + /// - `token` - The token. + #[inline] + pub fn token_symbol(token: TokenId) -> Result> { + build_read_state(TOKEN_SYMBOL) + .input::() + .output::>, true>() + .handle_error_code::() + .call(&(token)) + } + + /// Returns the decimals for the specified token. + /// + /// # Parameters + /// - `token` - The token. + #[inline] + pub fn token_decimals(token: TokenId) -> Result { + build_read_state(TOKEN_DECIMALS) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(token)) + } +} + +/// The interface for creating, managing and destroying fungible tokens. +pub mod management { + use super::*; + + /// Create a new token with a given identifier. + /// + /// # Parameters + /// - `id` - The identifier of the token. + /// - `admin` - The account that will administer the token. + /// - `min_balance` - The minimum balance required for accounts holding this token. + #[inline] + pub fn create(id: TokenId, admin: AccountId, min_balance: Balance) -> Result<()> { + build_dispatch(CREATE) + .input::<(TokenId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, admin, min_balance)) + } + + /// Start the process of destroying a token. + /// + /// # Parameters + /// - `token` - The token to be destroyed. + #[inline] + pub fn start_destroy(token: TokenId) -> Result<()> { + build_dispatch(START_DESTROY) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(token)) + } + + /// Set the metadata for a token. + /// + /// # Parameters + /// - `token`: The token to update. + /// - `name`: The user friendly name of this token. + /// - `symbol`: The exchange symbol for this token. + /// - `decimals`: The number of decimals this token uses to represent one unit. + #[inline] + pub fn set_metadata( + token: TokenId, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> Result<()> { + build_dispatch(SET_METADATA) + .input::<(TokenId, Vec, Vec, u8)>() + .output::, true>() + .handle_error_code::() + .call(&(token, name, symbol, decimals)) + } + + /// Clear the metadata for a token. + /// + /// # Parameters + /// - `token` - The token to update + #[inline] + pub fn clear_metadata(token: TokenId) -> Result<()> { + build_dispatch(CLEAR_METADATA) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(token)) + } + + /// Checks if a specified token exists. + /// + /// # Parameters + /// - `token` - The token. + #[inline] + pub fn token_exists(token: TokenId) -> Result { + build_read_state(TOKEN_EXISTS) + .input::() + .output::, true>() + .handle_error_code::() + .call(&(token)) + } +} + +/// Represents various errors related to fungible tokens. +/// +/// The `FungiblesError` provides a detailed and specific set of error types that can occur when +/// interacting with fungible tokens. Each variant signifies a particular error +/// condition, facilitating precise error handling and debugging. +/// +/// It is designed to be lightweight, including only the essential errors relevant to fungible token +/// operations. The `Other` variant serves as a catch-all for any unexpected errors. For more +/// detailed debugging, the `Other` variant can be converted into the richer `Error` type defined in +/// the primitives crate. +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum FungiblesError { + /// An unspecified or unknown error occurred. + Other(StatusCode), + /// The token is not live; either frozen or being destroyed. + NotLive, + /// Not enough allowance to fulfill a request is available. + InsufficientAllowance, + /// Not enough balance to fulfill a request is available. + InsufficientBalance, + /// The token ID is already taken. + InUse, + /// Minimum balance should be non-zero. + MinBalanceZero, + /// The account to alter does not exist. + NoAccount, + /// The signing account has no permission to do the operation. + NoPermission, + /// The given token ID is unknown. + Unknown, + /// No balance for creation of tokens or fees. + // TODO: Originally `pallet_balances::Error::InsufficientBalance` but collides with the + // `InsufficientBalance` error that is used for `pallet_assets::Error::BalanceLow` to adhere + // to the standard. This deserves a second look. + NoBalance, +} + +impl From for FungiblesError { + /// Converts a `StatusCode` to a `FungiblesError`. + /// + /// This conversion maps a `StatusCode`, returned by the runtime, to a more descriptive + /// `FungiblesError`. This provides better context and understanding of the error, allowing + /// developers to handle the most important errors effectively. + fn from(value: StatusCode) -> Self { + let encoded = value.0.to_le_bytes(); + match encoded { + // Balances. + [_, BALANCES, 2, _] => FungiblesError::NoBalance, + // Assets. + [_, ASSETS, 0, _] => FungiblesError::NoAccount, + [_, ASSETS, 1, _] => FungiblesError::NoPermission, + [_, ASSETS, 2, _] => FungiblesError::Unknown, + [_, ASSETS, 3, _] => FungiblesError::InUse, + [_, ASSETS, 5, _] => FungiblesError::MinBalanceZero, + [_, ASSETS, 7, _] => FungiblesError::InsufficientAllowance, + [_, ASSETS, 10, _] => FungiblesError::NotLive, + _ => FungiblesError::Other(value), + } + } +} + +#[cfg(test)] +mod tests { + use ink::scale::{Decode, Encode}; + + use super::FungiblesError; + use crate::{ + constants::{ASSETS, BALANCES}, + primitives::{ + ArithmeticError::*, + Error::{self, *}, + TokenError::*, + TransactionalError::*, + }, + StatusCode, + }; + + fn error_into_status_code(error: Error) -> StatusCode { + let mut encoded_error = error.encode(); + encoded_error.resize(4, 0); + let value = u32::from_le_bytes( + encoded_error.try_into().expect("qed, resized to 4 bytes line above"), + ); + value.into() + } + + fn into_fungibles_error(error: Error) -> FungiblesError { + let status_code: StatusCode = error_into_status_code(error); + status_code.into() + } + + // If we ever want to change the conversion from bytes to `u32`. + #[test] + fn status_code_vs_encoded() { + assert_eq!(u32::decode(&mut &[3u8, 10, 2, 0][..]).unwrap(), 133635u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 0, 0][..]).unwrap(), 13315u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 1, 0][..]).unwrap(), 78851u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 2, 0][..]).unwrap(), 144387u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 3, 0][..]).unwrap(), 209923u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 5, 0][..]).unwrap(), 340995u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 7, 0][..]).unwrap(), 472067u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 10, 0][..]).unwrap(), 668675u32); + } + + #[test] + fn converting_status_code_into_fungibles_error_works() { + let other_errors = vec![ + Other, + CannotLookup, + BadOrigin, + // `ModuleError` other than assets module. + Module { index: 2, error: [5, 0] }, + ConsumerRemaining, + NoProviders, + TooManyConsumers, + Token(OnlyProvider), + Arithmetic(Overflow), + Transactional(NoLayer), + Exhausted, + Corruption, + Unavailable, + RootNotAllowed, + Unknown { dispatch_error_index: 5, error_index: 5, error: 1 }, + DecodingFailed, + ]; + for error in other_errors { + let status_code: StatusCode = error_into_status_code(error); + let fungibles_error: FungiblesError = status_code.into(); + assert_eq!(fungibles_error, FungiblesError::Other(status_code)) + } + + assert_eq!( + into_fungibles_error(Module { index: BALANCES, error: [2, 0] }), + FungiblesError::NoBalance + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [0, 0] }), + FungiblesError::NoAccount + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [1, 0] }), + FungiblesError::NoPermission + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [2, 0] }), + FungiblesError::Unknown + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [3, 0] }), + FungiblesError::InUse + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [5, 0] }), + FungiblesError::MinBalanceZero + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [7, 0] }), + FungiblesError::InsufficientAllowance + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [10, 0] }), + FungiblesError::NotLive + ); + } +} diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index 2ae0b821..c6153892 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -1,21 +1,36 @@ use crate::{ - primitives::storage_keys::{ParachainSystemKeys, RuntimeStateKeys}, - BlockNumber, PopApiError, + build_extension_method, + constants::{DISPATCH, READ_STATE}, + primitives::Error, + ChainExtensionMethodApi, StatusCode, }; -pub mod balances; -pub mod cross_chain; -pub mod nfts; -pub mod state; +/// APIs for fungible tokens. +#[cfg(feature = "fungibles")] +pub mod fungibles; -pub fn relay_chain_block_number() -> Result { - state::read(RuntimeStateKeys::ParachainSystem(ParachainSystemKeys::LastRelayChainBlockNumber)) +pub(crate) const V0: u8 = 0; + +impl From for Error { + fn from(value: StatusCode) -> Self { + value.0.into() + } +} + +// Helper method to build a dispatch call. +// +// Parameters: +// - 'module': The index of the runtime module. +// - 'dispatchable': The index of the module dispatchable functions. +fn build_dispatch(module: u8, dispatchable: u8) -> ChainExtensionMethodApi { + build_extension_method(DISPATCH, V0, module, dispatchable) } -#[derive(scale::Encode)] -pub(crate) enum RuntimeCall { - #[codec(index = 10)] - Balances(balances::BalancesCall), - #[codec(index = 50)] - Nfts(nfts::NftCalls), +// Helper method to build a call to read state. +// +// Parameters: +// - 'module': The index of the runtime module. +// - 'state_query': The index of the runtime state query. +fn build_read_state(module: u8, state_query: u8) -> ChainExtensionMethodApi { + build_extension_method(READ_STATE, V0, module, state_query) } diff --git a/pop-api/src/v0/nfts.rs b/pop-api/src/v0/nfts.rs deleted file mode 100644 index 3db08cd1..00000000 --- a/pop-api/src/v0/nfts.rs +++ /dev/null @@ -1,882 +0,0 @@ -use super::RuntimeCall; -use crate::{PopApiError::UnknownStatusCode, *}; -use ink::prelude::vec::Vec; -use primitives::{ApprovalsLimit, BoundedBTreeMap, KeyLimit, MultiAddress}; -pub use primitives::{CollectionId, ItemId}; -use scale::Encode; -pub use types::*; - -type Result = core::result::Result; - -/// Issue a new collection of non-fungible items -pub fn create( - admin: impl Into>, - config: CollectionConfig, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Create { admin: admin.into(), config }))?) -} - -/// Destroy a collection of fungible items. -pub fn destroy(collection: CollectionId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Destroy { collection }))?) -} - -/// Mint an item of a particular collection. -pub fn mint( - collection: CollectionId, - item: ItemId, - mint_to: impl Into>, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Mint { - collection, - item, - mint_to: mint_to.into(), - witness_data: None, - }))?) -} - -/// Destroy a single item. -pub fn burn(collection: CollectionId, item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Burn { collection, item }))?) -} - -/// Move an item from the sender account to another. -pub fn transfer( - collection: CollectionId, - item: ItemId, - dest: impl Into>, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Transfer { collection, item, dest: dest.into() }))?) -} - -/// Re-evaluate the deposits on some items. -pub fn redeposit(collection: CollectionId, items: Vec) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::Redeposit { collection, items }))?) -} - -/// Change the Owner of a collection. -pub fn transfer_ownership( - collection: CollectionId, - new_owner: impl Into>, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::TransferOwnership { - collection, - new_owner: new_owner.into(), - }))?) -} - -/// Set (or reset) the acceptance of ownership for a particular account. -pub fn set_accept_ownership( - collection: CollectionId, - maybe_collection: Option, -) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetAcceptOwnership { collection, maybe_collection }))?) -} - -/// Set the maximum number of items a collection could have. -pub fn set_collection_max_supply(collection: CollectionId, max_supply: u32) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetCollectionMaxSupply { collection, max_supply }))?) -} - -/// Update mint settings. -pub fn update_mint_settings(collection: CollectionId, mint_settings: MintSettings) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::UpdateMintSettings { collection, mint_settings }))?) -} - -/// Get the owner of the item, if the item exists. -pub fn owner(collection: CollectionId, item: ItemId) -> Result> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Owner(collection, item)))?) -} - -/// Get the owner of the collection, if the collection exists. -pub fn collection_owner(collection: CollectionId) -> Result> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CollectionOwner(collection)))?) -} - -/// Get the details of a collection. -pub fn collection(collection: CollectionId) -> Result> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Collection(collection)))?) -} - -/// Get the details of an item. -pub fn item(collection: CollectionId, item: ItemId) -> Result> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Item(collection, item)))?) -} - -pub mod approvals { - use super::*; - - /// Approve an item to be transferred by a delegated third-party account. - pub fn approve_transfer( - collection: CollectionId, - item: ItemId, - delegate: impl Into>, - maybe_deadline: Option, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ApproveTransfer { - collection, - item, - delegate: delegate.into(), - maybe_deadline, - }))?) - } - - /// Cancel one of the transfer approvals for a specific item. - pub fn cancel_approval( - collection: CollectionId, - item: ItemId, - delegate: impl Into>, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelApproval { - collection, - item, - delegate: delegate.into(), - }))?) - } - - /// Cancel all the approvals of a specific item. - pub fn clear_all_transfer_approvals(collection: CollectionId, item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearAllTransferApprovals { collection, item }))?) - } -} - -pub mod attributes { - use super::*; - - /// Approve item's attributes to be changed by a delegated third-party account. - pub fn approve_item_attribute( - collection: CollectionId, - item: ItemId, - delegate: impl Into>, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ApproveItemAttributes { - collection, - item, - delegate: delegate.into(), - }))?) - } - - /// Cancel the previously provided approval to change item's attributes. - pub fn cancel_item_attributes_approval( - collection: CollectionId, - item: ItemId, - delegate: impl Into>, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelItemAttributesApproval { - collection, - item, - delegate: delegate.into(), - }))?) - } - - /// Set an attribute for a collection or item. - pub fn set_attribute( - collection: CollectionId, - maybe_item: Option, - namespace: AttributeNamespace, - key: BoundedVec, - value: BoundedVec, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetAttribute { - collection, - maybe_item, - namespace, - key, - value, - }))?) - } - - /// Clear an attribute for a collection or item. - pub fn clear_attribute( - collection: CollectionId, - maybe_item: Option, - namespace: AttributeNamespace, - key: BoundedVec, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearAttribute { - collection, - maybe_item, - namespace, - key, - }))?) - } - - /// Get the attribute value of `item` of `collection` corresponding to `key`. - pub fn attribute( - collection: CollectionId, - item: ItemId, - key: BoundedVec, - ) -> Result>> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::Attribute(collection, item, key)))?) - } - - // /// Get the custom attribute value of `item` of `collection` corresponding to `key`. - // pub fn custom_attribute( - // account: AccountId, - // collection: CollectionId, - // item: ItemId, - // key: BoundedVec, - // ) -> Result>> { - // Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CustomAttribute( - // account, collection, item, key, - // )))?) - // } - - /// Get the system attribute value of `item` of `collection` corresponding to `key` if - /// `item` is `Some`. Otherwise, returns the system attribute value of `collection` - /// corresponding to `key`. - pub fn system_attribute( - collection: CollectionId, - item: Option, - key: BoundedVec, - ) -> Result>> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::SystemAttribute(collection, item, key)))?) - } - - /// Get the attribute value of `item` of `collection` corresponding to `key`. - pub fn collection_attribute( - collection: CollectionId, - key: BoundedVec, - ) -> Result>> { - Ok(state::read(RuntimeStateKeys::Nfts(NftsKeys::CollectionAttribute(collection, key)))?) - } -} - -pub mod locking { - use super::*; - - /// Disallows changing the metadata or attributes of the item. - pub fn lock_item_properties( - collection: CollectionId, - item: ItemId, - lock_metadata: bool, - lock_attributes: bool, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockItemProperties { - collection, - item, - lock_metadata, - lock_attributes, - }))?) - } - - /// Disallow further unprivileged transfer of an item. - pub fn lock_item_transfer(collection: CollectionId, item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockItemTransfer { collection, item }))?) - } - - /// Re-allow unprivileged transfer of an item. - pub fn unlock_item_transfer(collection: CollectionId, item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::UnlockItemTransfer { collection, item }))?) - } - - /// Disallows specified settings for the whole collection. - pub fn lock_collection( - collection: CollectionId, - lock_settings: CollectionSettings, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::LockCollection { collection, lock_settings }))?) - } -} - -pub mod metadata { - use super::*; - - /// Set the metadata for an item. - pub fn set_metadata( - collection: CollectionId, - item: ItemId, - data: BoundedVec, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetMetadata { collection, item, data }))?) - } - - /// Clear the metadata for an item. - pub fn clear_metadata(collection: CollectionId, item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearMetadata { collection, item }))?) - } - - /// Set the metadata for a collection. - pub fn set_collection_metadata( - collection: CollectionId, - data: BoundedVec, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetCollectionMetadata { collection, data }))?) - } - - /// Clear the metadata for a collection. - pub fn clear_collection_metadata(collection: CollectionId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClearCollectionMetadata { collection }))?) - } -} - -pub mod roles { - use super::*; - - /// Change the Issuer, Admin and Freezer of a collection. - pub fn set_team( - collection: CollectionId, - issuer: Option>>, - admin: Option>>, - freezer: Option>>, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetTeam { - collection, - issuer: issuer.map(|i| i.into()), - admin: admin.map(|i| i.into()), - freezer: freezer.map(|i| i.into()), - }))?) - } -} - -pub mod trading { - use super::*; - - /// Allows to pay the tips. - pub fn pay_tips(tips: BoundedVec) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::PayTips { tips }))?) - } - - /// Set (or reset) the price for an item. - pub fn price(collection: CollectionId, item: ItemId, price: Option) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::SetPrice { collection, item, price }))?) - } - - /// Allows to buy an item if it's up for sale. - pub fn buy_item(collection: CollectionId, item: ItemId, bid_price: Balance) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::BuyItem { collection, item, bid_price }))?) - } - - pub mod swaps { - use super::*; - - /// Register a new atomic swap, declaring an intention to send an `item` in exchange for - /// `desired_item` from origin to target on the current chain. - pub fn create_swap( - offered_collection: CollectionId, - offered_item: ItemId, - desired_collection: CollectionId, - maybe_desired_item: Option, - maybe_price: Option, - duration: BlockNumber, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::CreateSwap { - offered_collection, - offered_item, - desired_collection, - maybe_desired_item, - maybe_price, - duration, - }))?) - } - - /// Cancel an atomic swap. - pub fn cancel_swap(offered_collection: CollectionId, offered_item: ItemId) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::CancelSwap { - offered_collection, - offered_item, - }))?) - } - - /// Claim an atomic swap. - pub fn claim_swap( - send_collection: CollectionId, - send_item: ItemId, - receive_collection: CollectionId, - receive_item: ItemId, - ) -> Result<()> { - Ok(dispatch(RuntimeCall::Nfts(NftCalls::ClaimSwap { - send_collection, - send_item, - receive_collection, - receive_item, - }))?) - } - } -} - -#[derive(Encode)] -pub(crate) enum NftCalls { - #[codec(index = 0)] - Create { admin: MultiAddress, config: CollectionConfig }, - #[codec(index = 2)] - Destroy { collection: CollectionId }, - #[codec(index = 3)] - Mint { - collection: CollectionId, - item: ItemId, - mint_to: MultiAddress, - witness_data: Option<()>, - }, - #[codec(index = 5)] - Burn { collection: CollectionId, item: ItemId }, - #[codec(index = 6)] - Transfer { collection: CollectionId, item: ItemId, dest: MultiAddress }, - #[codec(index = 7)] - Redeposit { collection: CollectionId, items: Vec }, - #[codec(index = 8)] - LockItemTransfer { collection: CollectionId, item: ItemId }, - #[codec(index = 9)] - UnlockItemTransfer { collection: CollectionId, item: ItemId }, - #[codec(index = 10)] - LockCollection { collection: CollectionId, lock_settings: CollectionSettings }, - #[codec(index = 11)] - TransferOwnership { collection: CollectionId, new_owner: MultiAddress }, - #[codec(index = 12)] - SetTeam { - collection: CollectionId, - issuer: Option>, - admin: Option>, - freezer: Option>, - }, - #[codec(index = 15)] - ApproveTransfer { - collection: CollectionId, - item: ItemId, - delegate: MultiAddress, - maybe_deadline: Option, - }, - #[codec(index = 16)] - CancelApproval { collection: CollectionId, item: ItemId, delegate: MultiAddress }, - #[codec(index = 17)] - ClearAllTransferApprovals { collection: CollectionId, item: ItemId }, - #[codec(index = 18)] - LockItemProperties { - collection: CollectionId, - item: ItemId, - lock_metadata: bool, - lock_attributes: bool, - }, - #[codec(index = 19)] - SetAttribute { - collection: CollectionId, - maybe_item: Option, - namespace: AttributeNamespace, - key: BoundedVec, - value: BoundedVec, - }, - #[codec(index = 21)] - ClearAttribute { - collection: CollectionId, - maybe_item: Option, - namespace: AttributeNamespace, - key: BoundedVec, - }, - #[codec(index = 22)] - ApproveItemAttributes { - collection: CollectionId, - item: ItemId, - delegate: MultiAddress, - }, - #[codec(index = 23)] - CancelItemAttributesApproval { - collection: CollectionId, - item: ItemId, - delegate: MultiAddress, - }, - #[codec(index = 24)] - SetMetadata { collection: CollectionId, item: ItemId, data: BoundedVec }, - #[codec(index = 25)] - ClearMetadata { collection: CollectionId, item: ItemId }, - #[codec(index = 26)] - SetCollectionMetadata { collection: CollectionId, data: BoundedVec }, - #[codec(index = 27)] - ClearCollectionMetadata { collection: CollectionId }, - #[codec(index = 28)] - SetAcceptOwnership { collection: CollectionId, maybe_collection: Option }, - #[codec(index = 29)] - SetCollectionMaxSupply { collection: CollectionId, max_supply: u32 }, - #[codec(index = 30)] - UpdateMintSettings { collection: CollectionId, mint_settings: MintSettings }, - #[codec(index = 31)] - SetPrice { collection: CollectionId, item: ItemId, price: Option }, - #[codec(index = 32)] - BuyItem { collection: CollectionId, item: ItemId, bid_price: Balance }, - #[codec(index = 33)] - PayTips { tips: BoundedVec }, - #[codec(index = 34)] - CreateSwap { - offered_collection: CollectionId, - offered_item: ItemId, - desired_collection: CollectionId, - maybe_desired_item: Option, - maybe_price: Option, - duration: BlockNumber, - }, - #[codec(index = 35)] - CancelSwap { offered_collection: CollectionId, offered_item: ItemId }, - #[codec(index = 36)] - ClaimSwap { - send_collection: CollectionId, - send_item: ItemId, - receive_collection: CollectionId, - receive_item: ItemId, - }, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum Error { - /// The signing account has no permission to do the operation. - NoPermission, - /// The given item ID is unknown. - UnknownCollection, - /// The item ID has already been used for an item. - AlreadyExists, - /// The approval had a deadline that expired, so the approval isn't valid anymore. - ApprovalExpired, - /// The owner turned out to be different to what was expected. - WrongOwner, - /// The witness data given does not match the current state of the chain. - BadWitness, - /// Collection ID is already taken. - CollectionIdInUse, - /// Items within that collection are non-transferable. - ItemsNonTransferable, - /// The provided account is not a delegate. - NotDelegate, - /// The delegate turned out to be different to what was expected. - WrongDelegate, - /// No approval exists that would allow the transfer. - Unapproved, - /// The named owner has not signed ownership acceptance of the collection. - Unaccepted, - /// The item is locked (non-transferable). - ItemLocked, - /// Item's attributes are locked. - LockedItemAttributes, - /// Collection's attributes are locked. - LockedCollectionAttributes, - /// Item's metadata is locked. - LockedItemMetadata, - /// Collection's metadata is locked. - LockedCollectionMetadata, - /// All items have been minted. - MaxSupplyReached, - /// The max supply is locked and can't be changed. - MaxSupplyLocked, - /// The provided max supply is less than the number of items a collection already has. - MaxSupplyTooSmall, - /// The given item ID is unknown. - UnknownItem, - /// Swap doesn't exist. - UnknownSwap, - /// The given item has no metadata set. - MetadataNotFound, - /// The provided attribute can't be found. - AttributeNotFound, - /// Item is not for sale. - NotForSale, - /// The provided bid is too low. - BidTooLow, - /// The item has reached its approval limit. - ReachedApprovalLimit, - /// The deadline has already expired. - DeadlineExpired, - /// The duration provided should be less than or equal to `MaxDeadlineDuration`. - WrongDuration, - /// The method is disabled by system settings. - MethodDisabled, - /// The provided setting can't be set. - WrongSetting, - /// Item's config already exists and should be equal to the provided one. - InconsistentItemConfig, - /// Config for a collection or an item can't be found. - NoConfig, - /// Some roles were not cleared. - RolesNotCleared, - /// Mint has not started yet. - MintNotStarted, - /// Mint has already ended. - MintEnded, - /// The provided Item was already used for claiming. - AlreadyClaimed, - /// The provided data is incorrect. - IncorrectData, - /// The extrinsic was sent by the wrong origin. - WrongOrigin, - /// The provided signature is incorrect. - WrongSignature, - /// The provided metadata might be too long. - IncorrectMetadata, - /// Can't set more attributes per one call. - MaxAttributesLimitReached, - /// The provided namespace isn't supported in this call. - WrongNamespace, - /// Can't delete non-empty collections. - CollectionNotEmpty, - /// The witness data should be provided. - WitnessRequired, -} - -impl TryFrom for Error { - type Error = PopApiError; - - fn try_from(status_code: u32) -> core::result::Result { - use Error::*; - match status_code { - 0 => Ok(NoPermission), - 1 => Ok(UnknownCollection), - 2 => Ok(AlreadyExists), - 3 => Ok(ApprovalExpired), - 4 => Ok(WrongOwner), - 5 => Ok(BadWitness), - 6 => Ok(CollectionIdInUse), - 7 => Ok(ItemsNonTransferable), - 8 => Ok(NotDelegate), - 9 => Ok(WrongDelegate), - 10 => Ok(Unapproved), - 11 => Ok(Unaccepted), - 12 => Ok(ItemLocked), - 13 => Ok(LockedItemAttributes), - 14 => Ok(LockedCollectionAttributes), - 15 => Ok(LockedItemMetadata), - 16 => Ok(LockedCollectionMetadata), - 17 => Ok(MaxSupplyReached), - 18 => Ok(MaxSupplyLocked), - 19 => Ok(MaxSupplyTooSmall), - 20 => Ok(UnknownItem), - 21 => Ok(UnknownSwap), - 22 => Ok(MetadataNotFound), - 23 => Ok(AttributeNotFound), - 24 => Ok(NotForSale), - 25 => Ok(BidTooLow), - 26 => Ok(ReachedApprovalLimit), - 27 => Ok(DeadlineExpired), - 28 => Ok(WrongDuration), - 29 => Ok(MethodDisabled), - 30 => Ok(WrongSetting), - 31 => Ok(InconsistentItemConfig), - 32 => Ok(NoConfig), - 33 => Ok(RolesNotCleared), - 34 => Ok(MintNotStarted), - 35 => Ok(MintEnded), - 36 => Ok(AlreadyClaimed), - 37 => Ok(IncorrectData), - 38 => Ok(WrongOrigin), - 39 => Ok(WrongSignature), - 40 => Ok(IncorrectMetadata), - 41 => Ok(MaxAttributesLimitReached), - 42 => Ok(WrongNamespace), - 43 => Ok(CollectionNotEmpty), - 44 => Ok(WitnessRequired), - _ => Err(UnknownStatusCode(status_code)), - } - } -} - -impl From for Error { - fn from(error: PopApiError) -> Self { - match error { - PopApiError::Nfts(e) => e, - _ => panic!("expected nfts error"), - } - } -} - -// Local implementations of pallet-nfts types -mod types { - use super::*; - use crate::{ - primitives::{CollectionId, ItemId}, - Balance, BlockNumber, - }; - pub use enumflags2::{bitflags, BitFlags}; - use scale::{Decode, EncodeLike, MaxEncodedLen}; - use scale_info::{build::Fields, meta_type, prelude::vec, Path, Type, TypeInfo, TypeParameter}; - - /// Attribute namespaces for non-fungible tokens. - #[derive(Encode)] - pub enum AttributeNamespace { - /// An attribute was set by the pallet. - Pallet, - /// An attribute was set by collection's owner. - CollectionOwner, - /// An attribute was set by item's owner. - ItemOwner, - /// An attribute was set by pre-approved account. - Account(AccountId), - } - - /// Collection's configuration. - #[derive(Encode)] - pub struct CollectionConfig { - /// Collection's settings. - pub settings: CollectionSettings, - /// Collection's max supply. - pub max_supply: Option, - /// Default settings each item will get during the mint. - pub mint_settings: MintSettings, - } - - /// Information about a collection. - #[derive(Decode, Debug, Encode, Eq, PartialEq)] - pub struct CollectionDetails { - /// Collection's owner. - pub owner: AccountId, - /// The total balance deposited by the owner for all the storage data associated with this - /// collection. Used by `destroy`. - pub owner_deposit: Balance, - /// The total number of outstanding items of this collection. - pub items: u32, - /// The total number of outstanding item metadata of this collection. - pub item_metadatas: u32, - /// The total number of outstanding item configs of this collection. - pub item_configs: u32, - /// The total number of attributes for this collection. - pub attributes: u32, - } - - /// Wrapper type for `BitFlags` that implements `Codec`. - pub struct CollectionSettings(pub BitFlags); - - impl_codec_bitflags!(CollectionSettings, u64, CollectionSetting); - - /// Support for up to 64 user-enabled features on a collection. - #[bitflags] - #[repr(u64)] - #[derive(Copy, Clone, Encode, TypeInfo)] - pub enum CollectionSetting { - /// Items in this collection are transferable. - TransferableItems, - /// The metadata of this collection can be modified. - UnlockedMetadata, - /// Attributes of this collection can be modified. - UnlockedAttributes, - /// The supply of this collection can be modified. - UnlockedMaxSupply, - /// When this isn't set then the deposit is required to hold the items of this collection. - DepositRequired, - } - - /// Information concerning the ownership of a single unique item. - #[derive(Decode, Debug, Encode, Eq, PartialEq)] - pub struct ItemDetails { - /// The owner of this item. - pub owner: AccountId, - /// The approved transferrer of this item, if one is set. - pub approvals: BoundedBTreeMap, ApprovalsLimit>, - /// The amount held in the pallet's default account for this item. Free-hold items will - /// have this as zero. - pub deposit: Balance, - } - - /// Support for up to 64 user-enabled features on an item. - #[bitflags] - #[repr(u64)] - #[derive(Copy, Clone, Encode, TypeInfo)] - pub enum ItemSetting { - /// This item is transferable. - Transferable, - /// The metadata of this item can be modified. - UnlockedMetadata, - /// Attributes of this item can be modified. - UnlockedAttributes, - } - - /// Wrapper type for `BitFlags` that implements `Codec`. - pub struct ItemSettings(pub BitFlags); - - impl_codec_bitflags!(ItemSettings, u64, ItemSetting); - - /// Information about the tip. - #[derive(Encode)] - pub struct ItemTip { - /// The collection of the item. - pub(super) collection: CollectionId, - /// An item of which the tip is sent for. - pub(super) item: ItemId, - /// A sender of the tip. - pub(super) receiver: AccountId, - /// An amount the sender is willing to tip. - pub(super) amount: Balance, - } - - /// Holds the information about minting. - #[derive(Encode)] - pub struct MintSettings { - /// Whether anyone can mint or if minters are restricted to some subset. - pub mint_type: MintType, - /// An optional price per mint. - pub price: Option, - /// When the mint starts. - pub start_block: Option, - /// When the mint ends. - pub end_block: Option, - /// Default settings each item will get during the mint. - pub default_item_settings: ItemSettings, - } - - /// Mint type. Can the NFT be created by anyone, or only the creator of the collection, - /// or only by wallets that already hold an NFT from a certain collection? - /// The ownership of a privately minted NFT is still publicly visible. - #[derive(Encode)] - pub enum MintType { - /// Only an `Issuer` could mint items. - Issuer, - /// Anyone could mint items. - Public, - /// Only holders of items in specified collection could mint new items. - HolderOf(CollectionId), - } - - /// Holds the details about the price. - #[derive(Encode)] - pub struct PriceWithDirection { - /// An amount. - pub(super) amount: Balance, - /// A direction (send or receive). - pub(super) direction: PriceDirection, - } - - /// Specifies whether the tokens will be sent or received. - #[derive(Encode)] - pub enum PriceDirection { - /// Tokens will be sent. - Send, - /// Tokens will be received. - Receive, - } - - macro_rules! impl_codec_bitflags { - ($wrapper:ty, $size:ty, $bitflag_enum:ty) => { - impl MaxEncodedLen for $wrapper { - fn max_encoded_len() -> usize { - <$size>::max_encoded_len() - } - } - impl Encode for $wrapper { - fn using_encoded R>(&self, f: F) -> R { - self.0.bits().using_encoded(f) - } - } - impl EncodeLike for $wrapper {} - impl Decode for $wrapper { - fn decode( - input: &mut I, - ) -> core::result::Result { - let field = <$size>::decode(input)?; - Ok(Self(BitFlags::from_bits(field as $size).map_err(|_| "invalid value")?)) - } - } - - impl TypeInfo for $wrapper { - type Identity = Self; - - fn type_info() -> Type { - Type::builder() - .path(Path::new("BitFlags", module_path!())) - .type_params(vec![TypeParameter::new( - "T", - Some(meta_type::<$bitflag_enum>()), - )]) - .composite( - Fields::unnamed() - .field(|f| f.ty::<$size>().type_name(stringify!($bitflag_enum))), - ) - } - } - }; - } - pub(crate) use impl_codec_bitflags; -} diff --git a/pop-api/src/v0/state.rs b/pop-api/src/v0/state.rs deleted file mode 100644 index 9f5e4c0c..00000000 --- a/pop-api/src/v0/state.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::{primitives::storage_keys::RuntimeStateKeys, read_state, PopApiError}; -use scale::Decode; - -pub fn read(key: RuntimeStateKeys) -> crate::Result { - read_state(key).and_then(|v| T::decode(&mut &v[..]).map_err(|_e| PopApiError::DecodingFailed)) -} diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 67cf167d..e83ea72c 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -1,14 +1,17 @@ [package] +description = "Primitives crate for Pop" edition = "2021" license = "GPL-3.0-only" name = "pop-primitives" version = "0.0.0" [dependencies] -bounded-collections = { version = "0.1", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } +codec.workspace = true +scale-info.workspace = true + +[dev-dependencies] +enum-iterator = "2.1.0" [features] default = [ "std" ] -std = [ "bounded-collections/std", "scale-info/std", "scale/std" ] +std = [ "codec/std", "scale-info/std" ] diff --git a/primitives/README.md b/primitives/README.md new file mode 100644 index 00000000..ded7918a --- /dev/null +++ b/primitives/README.md @@ -0,0 +1 @@ +Reserved crate for pop-primitives. \ No newline at end of file diff --git a/primitives/src/cross_chain.rs b/primitives/src/cross_chain.rs deleted file mode 100644 index 381e6a61..00000000 --- a/primitives/src/cross_chain.rs +++ /dev/null @@ -1,19 +0,0 @@ -use scale::{Decode, Encode, MaxEncodedLen}; - -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum CrossChainMessage { - Relay(RelayChainMessage), -} - -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum RelayChainMessage { - // Rococo index: https://github.com/paritytech/polkadot-sdk/blob/629506ce061db76d31d4f7a81f4a497752b27259/polkadot/runtime/rococo/src/lib.rs#L1423 - #[codec(index = 66)] - OnDemand(OnDemand), -} - -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum OnDemand { - #[codec(index = 1)] - PlaceOrderKeepAlive { max_amount: u128, para_id: u32 }, -} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index dc79a1ed..8298fc5d 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -1,19 +1,199 @@ +//! The `pop-primitives` crate provides types used by other crates. + #![cfg_attr(not(feature = "std"), no_std, no_main)] -pub use bounded_collections::{BoundedBTreeMap, BoundedBTreeSet, BoundedVec, ConstU32}; -// use scale::{Decode, Encode, MaxEncodedLen}; - -pub mod cross_chain; -pub mod storage_keys; - -// /// Some way of identifying an account on the chain. -// #[derive(Encode, Decode, Debug, MaxEncodedLen)] -// pub struct AccountId([u8; 32]); -// Id used for identifying non-fungible collections. -pub type CollectionId = u32; -// Id used for identifying non-fungible items. -pub type ItemId = u32; -/// The maximum length of an attribute key. -pub type KeyLimit = ConstU32<64>; -/// The maximum approvals an item could have. -pub type ApprovalsLimit = ConstU32<20>; +use codec::{Decode, Encode}; +#[cfg(test)] +use enum_iterator::Sequence; +#[cfg(feature = "std")] +use scale_info::TypeInfo; +pub use v0::*; + +/// The identifier of a token. +pub type TokenId = u32; + +/// The first version of primitives' types. +pub mod v0 { + pub use error::*; + + use super::*; + + mod error { + use super::*; + + /// Reason why a call failed. + #[derive(Encode, Decode, Debug)] + #[cfg_attr(feature = "std", derive(TypeInfo, Eq, PartialEq, Clone))] + #[cfg_attr(test, derive(Sequence))] + #[repr(u8)] + #[allow(clippy::unnecessary_cast)] + pub enum Error { + /// Some error occurred. + Other = 0, + /// Failed to look up some data. + CannotLookup = 1, + /// A bad origin. + BadOrigin = 2, + /// A custom error in a module. + Module { + /// The pallet index. + index: u8, + /// The error within the pallet. + // Supports a single level of nested error only, due to status code type size + // constraints. + error: [u8; 2], + } = 3, + /// At least one consumer is remaining so the account cannot be destroyed. + ConsumerRemaining = 4, + /// There are no providers so the account cannot be created. + NoProviders = 5, + /// There are too many consumers so the account cannot be created. + TooManyConsumers = 6, + /// An error to do with tokens. + Token(TokenError) = 7, + /// An arithmetic error. + Arithmetic(ArithmeticError) = 8, + /// The number of transactional layers has been reached, or we are not in a + /// transactional layer. + Transactional(TransactionalError) = 9, + /// Resources exhausted, e.g. attempt to read/write data which is too large to + /// manipulate. + Exhausted = 10, + /// The state is corrupt; this is generally not going to fix itself. + Corruption = 11, + /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself + /// later. + Unavailable = 12, + /// Root origin is not allowed. + RootNotAllowed = 13, + /// Decoding failed. + DecodingFailed = 254, + /// An unknown error occurred. This variant captures any unexpected errors that the + /// contract cannot specifically handle. It is useful for cases where there are + /// breaking changes in the runtime or when an error falls outside the predefined + /// categories. + Unknown { + /// The index within the `DispatchError`. + dispatch_error_index: u8, + /// The index within the `DispatchError` variant (e.g. a `TokenError`). + error_index: u8, + /// The specific error code or sub-index, providing additional context (e.g. + /// `error` in `ModuleError`). + error: u8, + } = 255, + } + + impl From for Error { + /// Converts a `u32` status code into an `Error`. + /// + /// This conversion maps a raw status code returned by the runtime into the more + /// descriptive `Error` enum variant, providing better context and understanding of the + /// error. + fn from(value: u32) -> Self { + let encoded = value.to_le_bytes(); + Error::decode(&mut &encoded[..]).unwrap_or(Error::DecodingFailed) + } + } + + impl From for u32 { + fn from(value: Error) -> Self { + let mut encoded_error = value.encode(); + // Resize the encoded value to 4 bytes in order to decode the value into a u32 (4 + // bytes). + encoded_error.resize(4, 0); + u32::from_le_bytes( + encoded_error.try_into().expect("qed, resized to 4 bytes line above"), + ) + } + } + + /// Description of what went wrong when trying to complete an operation on a token. + #[derive(Encode, Decode, Debug)] + #[cfg_attr(test, derive(Sequence))] + #[cfg_attr(feature = "std", derive(TypeInfo, Eq, PartialEq, Clone))] + pub enum TokenError { + /// Funds are unavailable. + FundsUnavailable, + /// Some part of the balance gives the only provider reference to the account and thus + /// cannot be (re)moved. + OnlyProvider, + /// Account cannot exist with the funds that would be given. + BelowMinimum, + /// Account cannot be created. + CannotCreate, + /// The asset in question is unknown. + UnknownAsset, + /// Funds exist but are frozen. + Frozen, + /// Operation is not supported by the asset. + Unsupported, + /// Account cannot be created for a held balance. + CannotCreateHold, + /// Withdrawal would cause unwanted loss of account. + NotExpendable, + /// Account cannot receive the assets. + Blocked, + } + + /// Arithmetic errors. + #[derive(Encode, Decode, Debug)] + #[cfg_attr(test, derive(Sequence))] + #[cfg_attr(feature = "std", derive(TypeInfo, Eq, PartialEq, Clone))] + pub enum ArithmeticError { + /// Underflow. + Underflow, + /// Overflow. + Overflow, + /// Division by zero. + DivisionByZero, + } + + /// Errors related to transactional storage layers. + #[derive(Encode, Decode, Debug)] + #[cfg_attr(test, derive(Sequence))] + #[cfg_attr(feature = "std", derive(TypeInfo, Eq, PartialEq, Clone))] + pub enum TransactionalError { + /// Too many transactional layers have been spawned. + LimitReached, + /// A transactional layer was expected, but does not exist. + NoLayer, + } + } + + #[cfg(test)] + mod tests { + use enum_iterator::all; + + use super::{Error::*, *}; + + // Conversion method for `Error` to `u32`. + fn convert_error_into_u32(error: &Error) -> u32 { + let mut encoded_error = error.encode(); + encoded_error.resize(4, 0); + u32::from_le_bytes( + encoded_error.try_into().expect("qed, resized to 4 bytes line above"), + ) + } + + #[test] + fn test_error_u32_conversion_with_all_variants() { + // Test conversion for all Error variants + all::().collect::>().into_iter().for_each(|error| { + let status_code = u32::from(error.clone()); + let expected = convert_error_into_u32(&error); + assert_eq!(status_code, expected); + let decoded_error = Error::from(status_code); + assert_eq!(decoded_error, error); + }); + } + + #[test] + fn test_invalid_u32_values_result_in_decoding_failed() { + // U32 values that don't map to a valid Error. + vec![111u32, 999u32, 1234u32].into_iter().for_each(|invalid_value| { + let error: Error = invalid_value.into(); + assert_eq!(error, DecodingFailed,); + }); + } + } +} diff --git a/primitives/src/storage_keys.rs b/primitives/src/storage_keys.rs deleted file mode 100644 index 657d9035..00000000 --- a/primitives/src/storage_keys.rs +++ /dev/null @@ -1,36 +0,0 @@ -use scale::{Decode, Encode, MaxEncodedLen}; - -use super::*; - -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum RuntimeStateKeys { - Nfts(NftsKeys), - ParachainSystem(ParachainSystemKeys), -} - -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum ParachainSystemKeys { - /// Get the last relay chain block number seen by the parachain. - LastRelayChainBlockNumber, -} - -// https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/nfts/src/impl_nonfungibles.rs -#[derive(Encode, Decode, Debug, MaxEncodedLen)] -pub enum NftsKeys { - // Get the details of a collection. - Collection(CollectionId), - /// Get the owner of the collection, if the collection exists. - CollectionOwner(CollectionId), - // Get the details of an item. - Item(CollectionId, ItemId), - /// Get the owner of the item, if the item exists. - Owner(CollectionId, ItemId), - /// Get the attribute value of `item` of `collection` corresponding to `key`. - Attribute(CollectionId, ItemId, BoundedVec), - // /// Get the custom attribute value of `item` of `collection` corresponding to `key`. - // CustomAttribute(AccountId, CollectionId, ItemId, BoundedVec), - /// Get the system attribute value of `item` of `collection` corresponding to `key` - SystemAttribute(CollectionId, Option, BoundedVec), - /// Get the attribute value of `item` of `collection` corresponding to `key`. - CollectionAttribute(CollectionId, BoundedVec), -} diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 74ec5e3d..a94e2831 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -22,8 +22,10 @@ scale-info.workspace = true smallvec.workspace = true # Local +pallet-api.workspace = true +pop-chain-extension.workspace = true pop-primitives.workspace = true -pop-runtime-common = { workspace = true, default-features = false } +pop-runtime-common.workspace = true # Substrate frame-benchmarking.workspace = true @@ -90,9 +92,9 @@ parachain-info.workspace = true parachains-common.workspace = true [dev-dependencies] -enumflags2 = "0.7.9" -env_logger = "0.11.2" -hex = "0.4.3" +enumflags2.workspace = true +env_logger.workspace = true +hex.workspace = true [features] default = [ "std" ] @@ -116,6 +118,7 @@ std = [ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-api/std", "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", @@ -141,6 +144,7 @@ std = [ "parachains-common/std", "polkadot-parachain-primitives/std", "polkadot-runtime-common/std", + "pop-chain-extension/std", "pop-primitives/std", "pop-runtime-common/std", "scale-info/std", @@ -172,6 +176,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", + "pallet-api/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -190,6 +195,7 @@ runtime-benchmarks = [ "parachains-common/runtime-benchmarks", "polkadot-parachain-primitives/runtime-benchmarks", "polkadot-runtime-common/runtime-benchmarks", + "pop-chain-extension/runtime-benchmarks", "pop-runtime-common/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "xcm-builder/runtime-benchmarks", @@ -205,6 +211,7 @@ try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", "frame-try-runtime/try-runtime", + "pallet-api/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", diff --git a/runtime/devnet/src/config/api/mod.rs b/runtime/devnet/src/config/api/mod.rs new file mode 100644 index 00000000..035a6014 --- /dev/null +++ b/runtime/devnet/src/config/api/mod.rs @@ -0,0 +1,248 @@ +use core::marker::PhantomData; + +use codec::Decode; +use cumulus_primitives_core::Weight; +use frame_support::traits::Contains; +pub(crate) use pallet_api::Extension; +use pallet_api::{extension::*, Read}; +use sp_core::ConstU8; +use sp_runtime::DispatchError; +use sp_std::vec::Vec; +use versioning::*; + +use crate::{ + config::assets::TrustBackedAssetsInstance, fungibles, Runtime, RuntimeCall, RuntimeEvent, +}; + +mod versioning; + +type DecodingFailedError = DecodingFailed; +type DecodesAs = pallet_api::extension::DecodesAs< + Output, + ContractWeightsOf, + DecodingFailedError, + Logger, +>; + +/// A query of runtime state. +#[derive(Decode, Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +#[repr(u8)] +pub enum RuntimeRead { + /// Fungible token queries. + #[codec(index = 150)] + Fungibles(fungibles::Read), +} + +impl Readable for RuntimeRead { + /// The corresponding type carrying the result of the query for runtime state. + type Result = RuntimeResult; + + /// Determines the weight of the read, used to charge the appropriate weight before the read is + /// performed. + fn weight(&self) -> Weight { + match self { + RuntimeRead::Fungibles(key) => fungibles::Pallet::weight(key), + } + } + + /// Performs the read and returns the result. + fn read(self) -> Self::Result { + match self { + RuntimeRead::Fungibles(key) => RuntimeResult::Fungibles(fungibles::Pallet::read(key)), + } + } +} + +/// The result of a runtime state read. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +pub enum RuntimeResult { + /// Fungible token read results. + Fungibles(fungibles::ReadResult), +} + +impl RuntimeResult { + /// Encodes the result. + fn encode(&self) -> Vec { + match self { + RuntimeResult::Fungibles(result) => result.encode(), + } + } +} + +impl fungibles::Config for Runtime { + type AssetsInstance = TrustBackedAssetsInstance; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = fungibles::weights::SubstrateWeight; +} + +#[derive(Default)] +pub struct Config; +impl pallet_api::extension::Config for Config { + /// Functions used by the Pop API. + /// + /// Each function corresponds to specific functionality provided by the API, facilitating the + /// interaction between smart contracts and the runtime. + type Functions = ( + // Dispatching calls + DispatchCall< + // Function ID: 0. + IdentifiedByFirstByteOfFunctionId>, + // The runtime configuration. + Runtime, + // Decode as a versioned runtime call. + DecodesAs, + // Apply any filtering. + Filter, + // Ensure errors are versioned. + VersionedErrorConverter, + // Logging with a specific target. + DispatchCallLogTarget, + >, + // Reading state + ReadState< + // Function ID: 1. + IdentifiedByFirstByteOfFunctionId>, + // The runtime configuration. + Runtime, + // The runtime state reads available. + RuntimeRead, + // Decode as a versioned runtime read. + DecodesAs, + // Apply any filtering. + Filter, + // Convert the result of a read into the expected versioned result + VersionedResultConverter, + // Ensure errors are versioned. + VersionedErrorConverter, + // Logging with a specific target. + ReadStateLogTarget, + >, + ); + + /// The log target. + const LOG_TARGET: &'static str = LOG_TARGET; +} + +/// Filters used by the chain extension. +pub struct Filter(PhantomData); + +impl> Contains for Filter { + fn contains(c: &RuntimeCall) -> bool { + use fungibles::Call::*; + T::BaseCallFilter::contains(c) && + matches!( + c, + RuntimeCall::Fungibles( + transfer { .. } | + transfer_from { .. } | + approve { .. } | increase_allowance { .. } | + decrease_allowance { .. } | + create { .. } | set_metadata { .. } | + start_destroy { .. } | + clear_metadata { .. } | + mint { .. } | burn { .. } + ) + ) + } +} + +impl Contains for Filter { + fn contains(r: &RuntimeRead) -> bool { + use fungibles::Read::*; + matches!( + r, + RuntimeRead::Fungibles( + TotalSupply(..) | + BalanceOf { .. } | + Allowance { .. } | + TokenName(..) | TokenSymbol(..) | + TokenDecimals(..) | + TokenExists(..) + ) + ) + } +} + +#[cfg(test)] +mod tests { + use codec::Encode; + use pallet_api::fungibles::Call::*; + use sp_core::crypto::AccountId32; + use RuntimeCall::{Balances, Fungibles}; + + use super::*; + + const ACCOUNT: AccountId32 = AccountId32::new([0u8; 32]); + + #[test] + fn runtime_result_encode_works() { + let value = 1_000; + let result = fungibles::ReadResult::::TotalSupply(value); + assert_eq!(RuntimeResult::Fungibles(result).encode(), value.encode()); + } + + #[test] + fn filter_prevents_runtime_filtered_calls() { + use pallet_balances::{AdjustmentDirection, Call::*}; + use sp_runtime::MultiAddress; + + const CALLS: [RuntimeCall; 4] = [ + Balances(force_adjust_total_issuance { + direction: AdjustmentDirection::Increase, + delta: 0, + }), + Balances(force_set_balance { who: MultiAddress::Address32([0u8; 32]), new_free: 0 }), + Balances(force_transfer { + source: MultiAddress::Address32([0u8; 32]), + dest: MultiAddress::Address32([0u8; 32]), + value: 0, + }), + Balances(force_unreserve { who: MultiAddress::Address32([0u8; 32]), amount: 0 }), + ]; + + for call in CALLS { + assert!(!Filter::::contains(&call)) + } + } + + #[test] + fn filter_allows_fungibles_calls() { + const CALLS: [RuntimeCall; 11] = [ + Fungibles(transfer { token: 0, to: ACCOUNT, value: 0 }), + Fungibles(transfer_from { token: 0, from: ACCOUNT, to: ACCOUNT, value: 0 }), + Fungibles(approve { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(increase_allowance { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(decrease_allowance { token: 0, spender: ACCOUNT, value: 0 }), + Fungibles(create { id: 0, admin: ACCOUNT, min_balance: 0 }), + Fungibles(set_metadata { token: 0, name: vec![], symbol: vec![], decimals: 0 }), + Fungibles(start_destroy { token: 0 }), + Fungibles(clear_metadata { token: 0 }), + Fungibles(mint { token: 0, account: ACCOUNT, value: 0 }), + Fungibles(burn { token: 0, account: ACCOUNT, value: 0 }), + ]; + + for call in CALLS { + assert!(Filter::::contains(&call)) + } + } + + #[test] + fn filter_allows_fungibles_reads() { + use super::{fungibles::Read::*, RuntimeRead::*}; + const READS: [RuntimeRead; 7] = [ + Fungibles(TotalSupply(1)), + Fungibles(BalanceOf { token: 1, owner: ACCOUNT }), + Fungibles(Allowance { token: 1, owner: ACCOUNT, spender: ACCOUNT }), + Fungibles(TokenName(1)), + Fungibles(TokenSymbol(1)), + Fungibles(TokenDecimals(10)), + Fungibles(TokenExists(1)), + ]; + + for read in READS { + assert!(Filter::::contains(&read)) + } + } +} diff --git a/runtime/devnet/src/config/api/versioning.rs b/runtime/devnet/src/config/api/versioning.rs new file mode 100644 index 00000000..daa2a0a9 --- /dev/null +++ b/runtime/devnet/src/config/api/versioning.rs @@ -0,0 +1,295 @@ +use sp_runtime::ModuleError; + +use super::*; + +type Version = u8; + +/// Versioned runtime calls. +#[derive(Decode, Debug)] +pub enum VersionedRuntimeCall { + /// Version zero of runtime calls. + #[codec(index = 0)] + V0(RuntimeCall), +} + +impl From for RuntimeCall { + fn from(value: VersionedRuntimeCall) -> Self { + // Allows mapping from some previous runtime call shape to a current valid runtime call + match value { + VersionedRuntimeCall::V0(call) => call, + } + } +} + +/// Versioned runtime state reads. +#[derive(Decode, Debug)] +pub enum VersionedRuntimeRead { + /// Version zero of runtime state reads. + #[codec(index = 0)] + V0(RuntimeRead), +} + +impl From for RuntimeRead { + fn from(value: VersionedRuntimeRead) -> Self { + // Allows mapping from some previous runtime read shape to a current valid runtime read + match value { + VersionedRuntimeRead::V0(read) => read, + } + } +} + +/// Versioned runtime state read results. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Clone))] +pub enum VersionedRuntimeResult { + /// Version zero of runtime read results. + V0(RuntimeResult), +} + +impl TryFrom<(RuntimeResult, Version)> for VersionedRuntimeResult { + type Error = DispatchError; + + fn try_from(value: (RuntimeResult, Version)) -> Result { + let (result, version) = value; + match version { + // Allows mapping from current `RuntimeResult` to a specific/prior version + 0 => Ok(VersionedRuntimeResult::V0(result)), + _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + } + } +} + +impl From for Vec { + fn from(result: VersionedRuntimeResult) -> Self { + match result { + // Simply unwrap and return the encoded result + VersionedRuntimeResult::V0(result) => result.encode(), + } + } +} + +/// Versioned errors. +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub enum VersionedError { + /// Version zero of errors. + V0(pop_primitives::v0::Error), +} + +impl TryFrom<(DispatchError, Version)> for VersionedError { + type Error = DispatchError; + + fn try_from(value: (DispatchError, Version)) -> Result { + let (error, version) = value; + match version { + // Allows mapping from current `DispatchError` to a specific/prior version of `Error` + 0 => Ok(VersionedError::V0(V0Error::from(error).0)), + _ => Err(pallet_contracts::Error::::DecodingFailed.into()), + } + } +} + +impl From for u32 { + fn from(value: VersionedError) -> Self { + match value { + VersionedError::V0(error) => error.into(), + } + } +} + +// Type for conversion to a versioned `pop_primitives::Error` to avoid taking a dependency of +// sp-runtime on pop-primitives. +struct V0Error(pop_primitives::v0::Error); +impl From for V0Error { + fn from(error: DispatchError) -> Self { + use pop_primitives::v0::*; + use sp_runtime::{ArithmeticError::*, TokenError::*, TransactionalError::*}; + use DispatchError::*; + // Mappings exist here to avoid taking a dependency of sp_runtime on pop-primitives + Self(match error { + Other(_message) => { + // Note: lossy conversion: message not used due to returned contract status code + // size limitation + Error::Other + }, + CannotLookup => Error::CannotLookup, + BadOrigin => Error::BadOrigin, + Module(error) => { + // Note: message not used + let ModuleError { index, error, message: _message } = error; + // Map `pallet-contracts::Error::DecodingFailed` to `Error::DecodingFailed` + if index as usize == + ::index() && + error == DECODING_FAILED_ERROR + { + Error::DecodingFailed + } else { + // Note: lossy conversion of error value due to returned contract status code + // size limitation + Error::Module { index, error: [error[0], error[1]] } + } + }, + ConsumerRemaining => Error::ConsumerRemaining, + NoProviders => Error::NoProviders, + TooManyConsumers => Error::TooManyConsumers, + Token(error) => Error::Token(match error { + FundsUnavailable => TokenError::FundsUnavailable, + OnlyProvider => TokenError::OnlyProvider, + BelowMinimum => TokenError::BelowMinimum, + CannotCreate => TokenError::CannotCreate, + UnknownAsset => TokenError::UnknownAsset, + Frozen => TokenError::Frozen, + Unsupported => TokenError::Unsupported, + CannotCreateHold => TokenError::CannotCreateHold, + NotExpendable => TokenError::NotExpendable, + Blocked => TokenError::Blocked, + }), + Arithmetic(error) => Error::Arithmetic(match error { + Underflow => ArithmeticError::Underflow, + Overflow => ArithmeticError::Overflow, + DivisionByZero => ArithmeticError::DivisionByZero, + }), + Transactional(error) => Error::Transactional(match error { + LimitReached => TransactionalError::LimitReached, + NoLayer => TransactionalError::NoLayer, + }), + Exhausted => Error::Exhausted, + Corruption => Error::Corruption, + Unavailable => Error::Unavailable, + RootNotAllowed => Error::RootNotAllowed, + }) + } +} + +#[cfg(test)] +mod tests { + use codec::Encode; + use frame_system::Call; + use pop_primitives::{ArithmeticError::*, Error, TokenError::*, TransactionalError::*}; + use sp_runtime::ModuleError; + use DispatchError::*; + + use super::*; + + #[test] + fn from_versioned_runtime_call_to_runtime_call_works() { + let call = + RuntimeCall::System(Call::remark_with_event { remark: "pop".as_bytes().to_vec() }); + assert_eq!(RuntimeCall::from(VersionedRuntimeCall::V0(call.clone())), call); + } + + #[test] + fn from_versioned_runtime_read_to_runtime_read_works() { + let read = RuntimeRead::Fungibles(fungibles::Read::::TotalSupply(42)); + assert_eq!(RuntimeRead::from(VersionedRuntimeRead::V0(read.clone())), read); + } + + #[test] + fn versioned_runtime_result_works() { + let result = RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(1_000)); + let v0 = 0; + assert_eq!( + VersionedRuntimeResult::try_from((result.clone(), v0)), + Ok(VersionedRuntimeResult::V0(result.clone())) + ); + } + + #[test] + fn versioned_runtime_result_fails() { + // Unknown version 1. + assert_eq!( + VersionedRuntimeResult::try_from(( + RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(1_000)), + 1 + )), + Err(pallet_contracts::Error::::DecodingFailed.into()) + ); + } + + #[test] + fn versioned_runtime_result_to_bytes_works() { + let value = 1_000; + let result = RuntimeResult::Fungibles(fungibles::ReadResult::::TotalSupply(value)); + assert_eq!(>::from(VersionedRuntimeResult::V0(result)), value.encode()); + } + + #[test] + fn versioned_error_works() { + let error = BadOrigin; + let v0 = 0; + + assert_eq!( + VersionedError::try_from((error, v0)), + Ok(VersionedError::V0(V0Error::from(error).0)) + ); + } + + #[test] + fn versioned_error_fails() { + // Unknown version 1. + assert_eq!( + VersionedError::try_from((BadOrigin, 1)), + Err(pallet_contracts::Error::::DecodingFailed.into()) + ); + } + + #[test] + fn versioned_error_to_u32_works() { + assert_eq!(u32::from(VersionedError::V0(Error::BadOrigin)), u32::from(Error::BadOrigin)); + } + + // Compare all the different `DispatchError` variants with the expected `Error`. + #[test] + fn from_dispatch_error_to_error_works() { + let test_cases = vec![ + (Other(""), (Error::Other)), + (Other("UnknownCall"), Error::Other), + (Other("DecodingFailed"), Error::Other), + (Other("Random"), (Error::Other)), + (CannotLookup, Error::CannotLookup), + (BadOrigin, Error::BadOrigin), + ( + Module(ModuleError { index: 1, error: [2, 0, 0, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 0] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 0, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 2, 0], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + ( + Module(ModuleError { index: 1, error: [2, 2, 2, 4], message: Some("hallo") }), + Error::Module { index: 1, error: [2, 2] }, + ), + (pallet_contracts::Error::::DecodingFailed.into(), Error::DecodingFailed), + (ConsumerRemaining, Error::ConsumerRemaining), + (NoProviders, Error::NoProviders), + (TooManyConsumers, Error::TooManyConsumers), + (Token(sp_runtime::TokenError::BelowMinimum), Error::Token(BelowMinimum)), + (Arithmetic(sp_runtime::ArithmeticError::Overflow), Error::Arithmetic(Overflow)), + ( + Transactional(sp_runtime::TransactionalError::LimitReached), + Error::Transactional(LimitReached), + ), + (Exhausted, Error::Exhausted), + (Corruption, Error::Corruption), + (Unavailable, Error::Unavailable), + (RootNotAllowed, Error::RootNotAllowed), + ]; + for (dispatch_error, expected) in test_cases { + let error = V0Error::from(dispatch_error).0; + assert_eq!(error, expected); + } + } + + #[test] + fn decoding_failed_error_encoding_works() { + let Module(error) = pallet_contracts::Error::::DecodingFailed.into() else { + unreachable!() + }; + assert_eq!(error.error, DECODING_FAILED_ERROR) + } +} diff --git a/runtime/devnet/src/config/assets.rs b/runtime/devnet/src/config/assets.rs index d66358a6..326b7e59 100644 --- a/runtime/devnet/src/config/assets.rs +++ b/runtime/devnet/src/config/assets.rs @@ -77,8 +77,8 @@ parameter_types! { } impl pallet_nft_fractionalization::Config for Runtime { - type AssetBalance = >::Balance; - type AssetId = >::AssetId; + type AssetBalance = >::Balance; + type AssetId = >::AssetId; type Assets = Assets; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -96,9 +96,9 @@ impl pallet_nft_fractionalization::Config for Runtime { type WeightInfo = pallet_nft_fractionalization::weights::SubstrateWeight; } -pub type TrustBackedAssets = pallet_assets::Instance1; -pub type TrustBackedAssetsCall = pallet_assets::Call; -impl pallet_assets::Config for Runtime { +pub(crate) type TrustBackedAssetsInstance = pallet_assets::Instance1; +pub type TrustBackedAssetsCall = pallet_assets::Call; +impl pallet_assets::Config for Runtime { type ApprovalDeposit = ApprovalDeposit; type AssetAccountDeposit = AssetAccountDeposit; type AssetDeposit = AssetDeposit; diff --git a/runtime/devnet/src/config/contracts.rs b/runtime/devnet/src/config/contracts.rs index 5b37455c..b3701498 100644 --- a/runtime/devnet/src/config/contracts.rs +++ b/runtime/devnet/src/config/contracts.rs @@ -1,22 +1,15 @@ use frame_support::{ parameter_types, - traits::{ConstBool, ConstU32, Randomness}, + traits::{ConstBool, ConstU32, Nothing, Randomness}, }; use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; +use super::api::{self, Config}; use crate::{ - deposit, extensions, Balance, Balances, BalancesCall, Perbill, Runtime, RuntimeCall, - RuntimeEvent, RuntimeHoldReason, Timestamp, + deposit, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, RuntimeHoldReason, + Timestamp, }; -pub enum AllowBalancesCall {} - -impl frame_support::traits::Contains for AllowBalancesCall { - fn contains(call: &RuntimeCall) -> bool { - matches!(call, RuntimeCall::Balances(BalancesCall::transfer_allow_death { .. })) - } -} - fn schedule() -> pallet_contracts::Schedule { pallet_contracts::Schedule { limits: pallet_contracts::Limits { @@ -48,15 +41,10 @@ parameter_types! { impl pallet_contracts::Config for Runtime { type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type ApiVersion = (); - /// The safest default is to allow no calls at all. - /// - /// Runtimes should whitelist dispatchables that are allowed to be called from contracts - /// and make sure they are stable. Dispatchables exposed to contracts are not allowed to - /// change because that would break already deployed contracts. The `RuntimeCall` structure - /// itself is not allowed to change the indices of existing pallets, too. - type CallFilter = AllowBalancesCall; + // IMPORTANT: only runtime calls through the api are allowed. + type CallFilter = Nothing; type CallStack = [pallet_contracts::Frame; 23]; - type ChainExtension = extensions::PopApiExtension; + type ChainExtension = api::Extension; type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; type Currency = Balances; type Debug = (); @@ -90,3 +78,13 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type Xcm = pallet_xcm::Pallet; } + +// IMPORTANT: only runtime calls through the api are allowed. +#[test] +fn contracts_prevents_runtime_calls() { + use std::any::TypeId; + assert_eq!( + TypeId::of::<::CallFilter>(), + TypeId::of::() + ); +} diff --git a/runtime/devnet/src/config/mod.rs b/runtime/devnet/src/config/mod.rs index a3a64c92..1ef83bc1 100644 --- a/runtime/devnet/src/config/mod.rs +++ b/runtime/devnet/src/config/mod.rs @@ -1,4 +1,6 @@ -mod assets; +mod api; +// Public due to pop api integration tests crate. +pub mod assets; mod contracts; mod proxy; // Public due to integration tests crate. diff --git a/runtime/devnet/src/extensions.rs b/runtime/devnet/src/extensions.rs deleted file mode 100644 index faead2bc..00000000 --- a/runtime/devnet/src/extensions.rs +++ /dev/null @@ -1,195 +0,0 @@ -use frame_support::{ - dispatch::{GetDispatchInfo, RawOrigin}, - pallet_prelude::*, - traits::{Contains, OriginTrait}, -}; -use pallet_contracts::{ - chain_extension::{ - BufInBufOutState, ChainExtension, ChargedAmount, Environment, Ext, InitState, RetVal, - }, - WeightInfo, -}; -use pop_primitives::storage_keys::RuntimeStateKeys; -use sp_core::crypto::UncheckedFrom; -use sp_runtime::{traits::Dispatchable, DispatchError}; -use sp_std::vec::Vec; - -use crate::{AccountId, AllowedApiCalls, RuntimeCall, RuntimeOrigin}; - -const LOG_TARGET: &str = "pop-api::extension"; - -#[derive(Default)] -pub struct PopApiExtension; - -impl ChainExtension for PopApiExtension -where - T: pallet_contracts::Config - + frame_system::Config< - RuntimeOrigin = RuntimeOrigin, - AccountId = AccountId, - RuntimeCall = RuntimeCall, - >, - T::AccountId: UncheckedFrom + AsRef<[u8]>, -{ - fn call(&mut self, env: Environment) -> Result - where - E: Ext, - { - log::debug!(target:LOG_TARGET, " extension called "); - match v0::FuncId::try_from(env.func_id())? { - v0::FuncId::Dispatch => { - match dispatch::(env) { - Ok(()) => Ok(RetVal::Converging(0)), - Err(DispatchError::Module(error)) => { - // encode status code = pallet index in runtime + error index, allowing for - // 999 errors - Ok(RetVal::Converging( - (error.index as u32 * 1_000) + u32::from_le_bytes(error.error), - )) - }, - Err(e) => Err(e), - } - }, - v0::FuncId::ReadState => { - read_state::(env)?; - Ok(RetVal::Converging(0)) - }, - } - } -} - -pub mod v0 { - #[derive(Debug)] - pub enum FuncId { - Dispatch, - ReadState, - } -} - -impl TryFrom for v0::FuncId { - type Error = DispatchError; - - fn try_from(func_id: u16) -> Result { - let id = match func_id { - 0x0 => Self::Dispatch, - 0x1 => Self::ReadState, - _ => { - log::error!("called an unregistered `func_id`: {:}", func_id); - return Err(DispatchError::Other("unimplemented func_id")); - }, - }; - - Ok(id) - } -} - -fn dispatch_call( - env: &mut Environment, - call: RuntimeCall, - mut origin: RuntimeOrigin, - log_prefix: &str, -) -> Result<(), DispatchError> -where - E: Ext, -{ - let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; - - log::debug!(target:LOG_TARGET, "{} inputted RuntimeCall: {:?}", log_prefix, call); - - origin.add_filter(AllowedApiCalls::contains); - - match call.dispatch(origin) { - Ok(info) => { - log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); - - // refund weight if the actual weight is less than the charged weight - if let Some(actual_weight) = info.actual_weight { - env.adjust_weight(charged_dispatch_weight, actual_weight); - } - - Ok(()) - }, - Err(err) => { - log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); - Err(err.error) - }, - } -} - -fn charge_overhead_weight( - env: &mut Environment, - len: u32, - log_prefix: &str, -) -> Result -where - T: pallet_contracts::Config, - E: Ext, -{ - // calculate weight for reading bytes of `len` - // reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=CopyToContract(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_input(len)%2C - let base_weight: Weight = T::WeightInfo::seal_return(len); - - // debug_message weight is a good approximation of the additional overhead of going - // from contract layer to substrate layer. - // reference: https://github.com/paritytech/polkadot-sdk/pull/4233/files#:~:text=DebugMessage(len)%20%3D%3E%20T%3A%3AWeightInfo%3A%3Aseal_debug_message(len)%2C - let overhead = T::WeightInfo::seal_debug_message(len); - - let charged_weight = env.charge_weight(base_weight.saturating_add(overhead))?; - log::debug!(target: LOG_TARGET, "{} charged weight: {:?}", log_prefix, charged_weight); - - Ok(charged_weight) -} - -fn dispatch(env: Environment) -> Result<(), DispatchError> -where - T: pallet_contracts::Config, - RuntimeOrigin: From>, - E: Ext, -{ - const LOG_PREFIX: &str = " dispatch |"; - - let mut env = env.buf_in_buf_out(); - let len = env.in_len(); - - charge_overhead_weight::(&mut env, len, LOG_PREFIX)?; - - // read the input as RuntimeCall - let call: RuntimeCall = env.read_as_unbounded(len)?; - - // contract is the origin by default - let origin: RuntimeOrigin = RawOrigin::Signed(env.ext().address().clone()).into(); - - dispatch_call::(&mut env, call, origin, LOG_PREFIX) -} - -fn read_state(env: Environment) -> Result<(), DispatchError> -where - T: pallet_contracts::Config, - E: Ext, -{ - const LOG_PREFIX: &str = " read_state |"; - - let mut env = env.buf_in_buf_out(); - - // To be conservative, we charge the weight for reading the input bytes of a fixed-size type. - let base_weight: Weight = T::WeightInfo::seal_return(env.in_len()); - let charged_weight = env.charge_weight(base_weight)?; - - log::debug!(target:LOG_TARGET, "{} charged weight: {:?}", LOG_PREFIX, charged_weight); - - let key: RuntimeStateKeys = env.read_as()?; - - let result = match key { - _ => Vec::::default(), - } - .encode(); - - log::trace!( - target:LOG_TARGET, - "{} result: {:?}.", LOG_PREFIX, result - ); - env.write(&result, false, None).map_err(|e| { - log::trace!(target: LOG_TARGET, "{:?}", e); - DispatchError::Other("unable to write results to contract memory") - }) -} diff --git a/runtime/devnet/src/lib.rs b/runtime/devnet/src/lib.rs index ca0e5137..1c799b96 100644 --- a/runtime/devnet/src/lib.rs +++ b/runtime/devnet/src/lib.rs @@ -8,7 +8,6 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); // Public due to integration tests crate. pub mod config; -mod extensions; mod weights; use config::xcm::{RelayLocation, XcmOriginToTransactDispatchOrigin}; @@ -34,6 +33,7 @@ use frame_system::{ limits::{BlockLength, BlockWeights}, EnsureRoot, }; +use pallet_api::fungibles; use pallet_balances::Call as BalancesCall; use pallet_xcm::{EnsureXcm, IsVoiceOfBody}; use parachains_common::message_queue::{NarrowOriginToSibling, ParaIdToSibling}; @@ -246,14 +246,6 @@ impl Contains for FilteredCalls { } } -/// A type to identify allowed calls to the Runtime from contracts. Used by Pop API -pub struct AllowedApiCalls; -impl Contains for AllowedApiCalls { - fn contains(_c: &RuntimeCall) -> bool { - false - } -} - /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from /// [`ParaChainDefaultConfig`](`struct@frame_system::config_preludes::ParaChainDefaultConfig`), /// but overridden as needed. @@ -627,12 +619,17 @@ mod runtime { pub type NftFractionalization = pallet_nft_fractionalization::Pallet; #[runtime::pallet_index(52)] pub type Assets = pallet_assets::Pallet; + + // Pop API + #[runtime::pallet_index(150)] + pub type Fungibles = fungibles::Pallet; } #[cfg(feature = "runtime-benchmarks")] mod benches { frame_benchmarking::define_benchmarks!( [frame_system, SystemBench::] + [fungibles, Fungibles] [pallet_balances, Balances] [pallet_session, SessionBench::] [pallet_timestamp, Timestamp] @@ -997,3 +994,14 @@ cumulus_pallet_parachain_system::register_validate_block! { Runtime = Runtime, BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::, } + +// Ensures that the account id lookup does not perform any state reads. When this changes, +// `pallet_api::fungibles` dispatchables need to be re-evaluated. +#[test] +fn test_lookup_config() { + use std::any::TypeId; + assert_eq!( + TypeId::of::<::Lookup>(), + TypeId::of::>() + ); +} diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index 2fc38023..6dc58e18 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -23,7 +23,7 @@ smallvec.workspace = true # Local pop-primitives.workspace = true -pop-runtime-common = { workspace = true, default-features = false } +pop-runtime-common.workspace = true # Substrate frame-benchmarking.workspace = true @@ -90,9 +90,9 @@ parachain-info.workspace = true parachains-common.workspace = true [dev-dependencies] -enumflags2 = "0.7.9" -env_logger = "0.11.2" -hex = "0.4.3" +enumflags2.workspace = true +env_logger.workspace = true +hex.workspace = true [features] default = [ "std" ] diff --git a/runtime/testnet/src/config/assets.rs b/runtime/testnet/src/config/assets.rs index d66358a6..91322ecf 100644 --- a/runtime/testnet/src/config/assets.rs +++ b/runtime/testnet/src/config/assets.rs @@ -77,8 +77,8 @@ parameter_types! { } impl pallet_nft_fractionalization::Config for Runtime { - type AssetBalance = >::Balance; - type AssetId = >::AssetId; + type AssetBalance = >::Balance; + type AssetId = >::AssetId; type Assets = Assets; #[cfg(feature = "runtime-benchmarks")] type BenchmarkHelper = (); @@ -96,9 +96,9 @@ impl pallet_nft_fractionalization::Config for Runtime { type WeightInfo = pallet_nft_fractionalization::weights::SubstrateWeight; } -pub type TrustBackedAssets = pallet_assets::Instance1; -pub type TrustBackedAssetsCall = pallet_assets::Call; -impl pallet_assets::Config for Runtime { +pub type TrustBackedAssetsInstance = pallet_assets::Instance1; +pub type TrustBackedAssetsCall = pallet_assets::Call; +impl pallet_assets::Config for Runtime { type ApprovalDeposit = ApprovalDeposit; type AssetAccountDeposit = AssetAccountDeposit; type AssetDeposit = AssetDeposit; diff --git a/runtime/testnet/src/config/contracts.rs b/runtime/testnet/src/config/contracts.rs index f052b1d3..5d3c7ee9 100644 --- a/runtime/testnet/src/config/contracts.rs +++ b/runtime/testnet/src/config/contracts.rs @@ -1,22 +1,14 @@ use frame_support::{ parameter_types, - traits::{ConstBool, ConstU32, Randomness}, + traits::{ConstBool, ConstU32, Nothing, Randomness}, }; use frame_system::{pallet_prelude::BlockNumberFor, EnsureSigned}; use crate::{ - deposit, extensions, Balance, Balances, BalancesCall, Perbill, Runtime, RuntimeCall, - RuntimeEvent, RuntimeHoldReason, Timestamp, + deposit, extensions, Balance, Balances, Perbill, Runtime, RuntimeCall, RuntimeEvent, + RuntimeHoldReason, Timestamp, }; -pub enum AllowBalancesCall {} - -impl frame_support::traits::Contains for AllowBalancesCall { - fn contains(call: &RuntimeCall) -> bool { - matches!(call, RuntimeCall::Balances(BalancesCall::transfer_allow_death { .. })) - } -} - fn schedule() -> pallet_contracts::Schedule { pallet_contracts::Schedule { limits: pallet_contracts::Limits { @@ -48,13 +40,8 @@ parameter_types! { impl pallet_contracts::Config for Runtime { type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type ApiVersion = (); - /// The safest default is to allow no calls at all. - /// - /// Runtimes should whitelist dispatchables that are allowed to be called from contracts - /// and make sure they are stable. Dispatchables exposed to contracts are not allowed to - /// change because that would break already deployed contracts. The `RuntimeCall` structure - /// itself is not allowed to change the indices of existing pallets, too. - type CallFilter = AllowBalancesCall; + // IMPORTANT: only runtime calls through the api are allowed. + type CallFilter = Nothing; type CallStack = [pallet_contracts::Frame; 23]; type ChainExtension = extensions::PopApiExtension; type CodeHashLockupDepositPercent = CodeHashLockupDepositPercent; @@ -90,3 +77,13 @@ impl pallet_contracts::Config for Runtime { type WeightPrice = pallet_transaction_payment::Pallet; type Xcm = pallet_xcm::Pallet; } + +// IMPORTANT: only runtime calls through the api are allowed. +#[test] +fn contracts_prevents_runtime_calls() { + use std::any::TypeId; + assert_eq!( + TypeId::of::<::CallFilter>(), + TypeId::of::() + ); +} diff --git a/runtime/testnet/src/extensions.rs b/runtime/testnet/src/extensions.rs index 3e84c834..084778eb 100644 --- a/runtime/testnet/src/extensions.rs +++ b/runtime/testnet/src/extensions.rs @@ -9,7 +9,6 @@ use pallet_contracts::{ }, WeightInfo, }; -use pop_primitives::storage_keys::RuntimeStateKeys; use sp_core::crypto::UncheckedFrom; use sp_runtime::{traits::Dispatchable, DispatchError}; use sp_std::vec::Vec; @@ -31,10 +30,10 @@ where >, T::AccountId: UncheckedFrom + AsRef<[u8]>, { - fn call(&mut self, env: Environment) -> Result - where - E: Ext, - { + fn call>( + &mut self, + env: Environment, + ) -> Result { log::debug!(target:LOG_TARGET, " extension called "); match v0::FuncId::try_from(env.func_id())? { v0::FuncId::Dispatch => { @@ -177,12 +176,9 @@ where log::debug!(target:LOG_TARGET, "{} charged weight: {:?}", LOG_PREFIX, charged_weight); - let key: RuntimeStateKeys = env.read_as()?; - - let result = match key { - _ => Vec::::default(), - } - .encode(); + // TODO: always returning an empty vec. Chainextension will be refactored into one for both + // runtimes before pop api implementation gets merged into main. + let result = Vec::::default().encode(); log::trace!( target:LOG_TARGET, diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index c39d9928..e773712e 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -1009,3 +1009,14 @@ cumulus_pallet_parachain_system::register_validate_block! { Runtime = Runtime, BlockExecutor = cumulus_pallet_aura_ext::BlockExecutor::, } + +// Ensures that the account id lookup does not perform any state reads. When this changes, +// `pallet_api::fungibles` dispatchables need to be re-evaluated. +#[test] +fn test_lookup_config() { + use std::any::TypeId; + assert_eq!( + TypeId::of::<::Lookup>(), + TypeId::of::>() + ); +} diff --git a/scripts/pallet-weights-template.hbs b/scripts/pallet-weights-template.hbs new file mode 100644 index 00000000..9e1e5a46 --- /dev/null +++ b/scripts/pallet-weights-template.hbs @@ -0,0 +1,122 @@ +{{header}} +//! Autogenerated weights for `{{pallet}}` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION {{version}} +//! DATE: {{date}}, STEPS: `{{cmd.steps}}`, REPEAT: `{{cmd.repeat}}`, LOW RANGE: `{{cmd.lowest_range_values}}`, HIGH RANGE: `{{cmd.highest_range_values}}` +//! WORST CASE MAP SIZE: `{{cmd.worst_case_map_values}}` +//! HOSTNAME: `R0GUE`, CPU: `{{cpuname}}` +//! WASM-EXECUTION: `{{cmd.wasm_execution}}`, CHAIN: `{{cmd.chain}}`, DB CACHE: `{{cmd.db_cache}}` + +// Executed Command: +{{#each args as |arg|}} +// {{arg}} +{{/each}} + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `{{pallet}}`. +pub trait WeightInfo { + {{#each benchmarks as |benchmark|}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{c.name}}: u32, {{/each~}} + ) -> Weight; + {{/each}} +} + +/// Weights for `{{pallet}}` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +{{#if (eq pallet "frame_system")}} +impl WeightInfo for SubstrateWeight { +{{else}} +impl WeightInfo for SubstrateWeight { +{{/if}} + {{#each benchmarks as |benchmark|}} + {{#each benchmark.comments as |comment|}} + /// {{comment}} + {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + // Proof Size summary in bytes: + // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Minimum execution time: {{underscore benchmark.min_execution_time}}_000 picoseconds. + Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) + {{#each benchmark.component_weight as |cw|}} + // Standard Error: {{underscore cw.error}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(T::DbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#each benchmark.component_reads as |cr|}} + .saturating_add(T::DbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) + {{/each}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(T::DbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(T::DbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} + {{#each benchmark.component_calculated_proof_size as |cp|}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} + } + {{/each}} +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + {{#each benchmarks as |benchmark|}} + {{#each benchmark.comments as |comment|}} + /// {{comment}} + {{/each}} + {{#each benchmark.component_ranges as |range|}} + /// The range of component `{{range.name}}` is `[{{range.min}}, {{range.max}}]`. + {{/each}} + fn {{benchmark.name~}} + ( + {{~#each benchmark.components as |c| ~}} + {{~#if (not c.is_used)}}_{{/if}}{{c.name}}: u32, {{/each~}} + ) -> Weight { + // Proof Size summary in bytes: + // Measured: `{{benchmark.base_recorded_proof_size}}{{#each benchmark.component_recorded_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Estimated: `{{benchmark.base_calculated_proof_size}}{{#each benchmark.component_calculated_proof_size as |cp|}} + {{cp.name}} * ({{cp.slope}} ±{{underscore cp.error}}){{/each}}` + // Minimum execution time: {{underscore benchmark.min_execution_time}}_000 picoseconds. + Weight::from_parts({{underscore benchmark.base_weight}}, {{benchmark.base_calculated_proof_size}}) + {{#each benchmark.component_weight as |cw|}} + // Standard Error: {{underscore cw.error}} + .saturating_add(Weight::from_parts({{underscore cw.slope}}, 0).saturating_mul({{cw.name}}.into())) + {{/each}} + {{#if (ne benchmark.base_reads "0")}} + .saturating_add(RocksDbWeight::get().reads({{benchmark.base_reads}}_u64)) + {{/if}} + {{#each benchmark.component_reads as |cr|}} + .saturating_add(RocksDbWeight::get().reads(({{cr.slope}}_u64).saturating_mul({{cr.name}}.into()))) + {{/each}} + {{#if (ne benchmark.base_writes "0")}} + .saturating_add(RocksDbWeight::get().writes({{benchmark.base_writes}}_u64)) + {{/if}} + {{#each benchmark.component_writes as |cw|}} + .saturating_add(RocksDbWeight::get().writes(({{cw.slope}}_u64).saturating_mul({{cw.name}}.into()))) + {{/each}} + {{#each benchmark.component_calculated_proof_size as |cp|}} + .saturating_add(Weight::from_parts(0, {{cp.slope}}).saturating_mul({{cp.name}}.into())) + {{/each}} + } + {{/each}} +} +