diff --git a/.github/workflows/ci-casper-js-client.yml b/.github/workflows/ci-casper-js-client.yml new file mode 100644 index 00000000..cce7fd6a --- /dev/null +++ b/.github/workflows/ci-casper-js-client.yml @@ -0,0 +1,95 @@ +name: ci-casper-js-client +on: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + push: + branches: + - master + - develop + paths-ignore: + - "**.md" + pull_request: + branches: + - master + - develop + - feature/* + paths-ignore: + - "**.md" + +env: + NODE_ENV: ci + WASM_RELEASE_PATH: ${{ github.workspace }}/target/wasm32-unknown-unknown/release + NCTL_USERS_FOLDER_PATH: /tmp/net-1/users + CHAIN_NAME: casper-net-1 + NODE_ADDRESS: http://localhost:11101/rpc + EVENT_STREAM_ADDRESS: http://localhost:18101/events/main + INSTALL_PAYMENT_AMOUNT: 200000000000 + DEPLOY_PAYMENT_AMOUNT: 200000000000 + +jobs: + run-e2e: + # The type of runner that the job will run on + runs-on: ubuntu-latest + # Service containers to run with `runner-job` + services: + # Label used to access the service container + casper-nctl: + # Docker Hub image + image: makesoftware/casper-nctl:v144-220303 + options: --name casper-nctl # -v ${{ github.workspace }}/assets:/home/casper/casper-node/utils/nctl/assets + env: + PREDEFINED_ACCOUNTS: 'true' + MINIMUM_ROUND_EXPONENT: '12' + MAXIMUM_ROUND_EXPONENT: '13' + DEPLOY_DELAY: '2sec' + ports: + # Opens RPC, REST and events ports on the host and service container + - 11101:11101 + - 14101:14101 + - 18101:18101 + volumes: + - /tmp:/home/casper/casper-node/utils/nctl/assets + steps: + - name: Fix keys permission + run: sudo chown runner $NCTL_USERS_FOLDER_PATH/*/* + + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache Build Wasm + id: wasm-cache + uses: actions/cache@v1 + with: + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + path: | + /home/runner/.cargo/bin + /home/runner/.cargo/git + /home/runner/.cargo/registry/cache + /home/runner/.cargo/registry/index + target + + - name: Build Wasm + if: steps.wasm-cache.outputs.cache-hit != 'true' + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: rustfmt, clippy + - run: sudo apt update && sudo apt install -y build-essential && sudo apt-get install wabt + if: steps.wasm-cache.outputs.cache-hit != 'true' + - run: make prepare + if: steps.wasm-cache.outputs.cache-hit != 'true' + - run: make build-contracts + if: steps.wasm-cache.outputs.cache-hit != 'true' + + - name: Setup NodeJS + uses: actions/setup-node@v2 + with: + node-version: "14" + + - name: Npm Install + run: cd client && npm install + + - name: E2E Test Report - Reputation Contract Client + run: cd client && NODE_ENV=ci npm run e2e:reputation diff --git a/.github/workflows/ci-casper-rust-contract.yml b/.github/workflows/ci-casper-rust-contract.yml new file mode 100644 index 00000000..068b99a8 --- /dev/null +++ b/.github/workflows/ci-casper-rust-contract.yml @@ -0,0 +1,32 @@ +name: ci-casper-rust-contract + +on: + push: + branches: + - master + - develop + paths-ignore: + - "**.md" + + pull_request: + branches: + - master + - develop + - feature/* + paths-ignore: + - "**.md" + +jobs: + build: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + components: rustfmt, clippy + - run: sudo apt update && sudo apt install -y build-essential + - run: make prepare + - run: make check-lint + - run: make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e850d35f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +tests/wasm/*.wasm \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..0ed81174 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1380 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98fcd36dda4e17b7d7abc64cb549bf0201f4ab71e00700c798ca7e62ed3761fa" +dependencies = [ + "funty", + "radium", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" +dependencies = [ + "crypto-mac 0.8.0", + "digest", + "opaque-debug", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "casper-contract" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e145bb5b2cde2f0c76b8571a03ef8a6de37616ef0e6e4b8a9bac937ff39c40" +dependencies = [ + "casper-types", + "hex_fmt", + "wee_alloc", +] + +[[package]] +name = "casper-dao-contracts" +version = "0.1.0" +dependencies = [ + "casper-contract", + "casper-dao-utils", + "casper-types", +] + +[[package]] +name = "casper-dao-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "casper-dao-utils" +version = "0.1.0" +dependencies = [ + "base64", + "casper-contract", + "casper-dao-macros", + "casper-engine-test-support", + "casper-execution-engine", + "casper-types", + "lazy_static", +] + +[[package]] +name = "casper-engine-test-support" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aeaa9950a9d0e778d0a94de98a323337b54e6c03bdeeafe44af19ea6005240" +dependencies = [ + "casper-execution-engine", + "casper-hashing", + "casper-types", + "lmdb", + "log", + "num-rational 0.4.0", + "num-traits", + "once_cell", + "rand 0.8.4", +] + +[[package]] +name = "casper-execution-engine" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dddee9d2f4e3dacdb7c69887cfe0ae1d2ffa6fa8af340d5d0ed4243ab21fbd24" +dependencies = [ + "anyhow", + "base16", + "bincode", + "casper-hashing", + "casper-types", + "chrono", + "datasize", + "hex-buffer-serde 0.2.2", + "hex_fmt", + "hostname", + "itertools", + "libc", + "linked-hash-map", + "lmdb", + "log", + "num", + "num-derive", + "num-rational 0.4.0", + "num-traits", + "once_cell", + "parity-wasm", + "proptest", + "pwasm-utils", + "rand 0.8.4", + "rand_chacha 0.3.1", + "schemars", + "serde", + "serde_bytes", + "serde_json", + "thiserror", + "tracing", + "uint", + "uuid", + "wasmi", +] + +[[package]] +name = "casper-hashing" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5af18f77cbabac69777333e0551accf1cd403995706953d4dabc56d7b56667a8" +dependencies = [ + "base16", + "blake2", + "casper-types", + "datasize", + "hex", + "hex-buffer-serde 0.3.0", + "itertools", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "casper-types" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9da1776d44d053d9b60040b7e70b448ce34608762e989cc1106d083125bf200" +dependencies = [ + "base16", + "base64", + "bitflags", + "blake2", + "datasize", + "ed25519-dalek", + "hex", + "hex_fmt", + "k256", + "num", + "num-derive", + "num-integer", + "num-rational 0.4.0", + "num-traits", + "once_cell", + "proptest", + "rand 0.8.4", + "schemars", + "serde", + "serde_bytes", + "serde_json", + "uint", +] + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "datasize" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cdaf1625dae32ea757a4a98e6f59496bb4fe80a41efb0bd57e631f6cb341770" +dependencies = [ + "datasize_derive", +] + +[[package]] +name = "datasize_derive" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1065db16e6dad1cfa0f50966d8405bb9f6d13f74f34d685b417f301cb32f1d86" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + +[[package]] +name = "ecdsa" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fbdb4ff710acb4db8ca29f93b897529ea6d6a45626d5183b47e012aa6ae7e4" +dependencies = [ + "elliptic-curve", + "hmac", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4620d40f6d2601794401d6dd95a5cf69b6c157852539470eeda433a99b3c0efc" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "sha2", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "elliptic-curve" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2db227e61a43a34915680bdda462ec0e212095518020a88a1f91acd16092c39" +dependencies = [ + "bitvec", + "digest", + "ff", + "funty", + "generic-array", + "group", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "ff" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01646e077d4ebda82b73f1bca002ea1e91561a77df2431a9e79729bcc31950ef" +dependencies = [ + "bitvec", + "rand_core 0.5.1", + "subtle", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc11f9f5fbf1943b48ae7c2bf6846e7d827a512d1be4f23af708f5ca5d01dde1" +dependencies = [ + "ff", + "rand_core 0.5.1", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-buffer-serde" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310e9578ff64e65a3a18e0624609f6833ee4a20503ef38eebb48430cf8ac3ab8" +dependencies = [ + "hex", + "serde", +] + +[[package]] +name = "hex-buffer-serde" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f52012c160668b4494727f3588045aa00429849fcae51de70d68fa98228039" +dependencies = [ + "hex", + "serde", +] + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac 0.10.1", + "digest", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "k256" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4476a0808212a9e81ce802eb1a0cfc60e73aea296553bacc0fac7e1268bc572a" +dependencies = [ + "cfg-if 1.0.0", + "ecdsa", + "elliptic-curve", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lmdb" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0908efb5d6496aa977d96f91413da2635a902e5e31dbef0bfb88986c248539" +dependencies = [ + "bitflags", + "libc", + "lmdb-sys", +] + +[[package]] +name = "lmdb-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b392838cfe8858e86fac37cf97a0e8c55cc60ba0a18365cadc33092f128ce9" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", + "serde", + "value-bag", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memory_units" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d96e3f3c0b6325d8ccd83c33b28acb183edcb6c67938ba104ec546854b0882" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint 0.4.3", + "num-complex", + "num-integer", + "num-iter", + "num-rational 0.4.0", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26873667bbbb7c5182d4a37c1add32cdf09f841af72da53318fdb81543c15085" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +dependencies = [ + "autocfg", + "num-bigint 0.4.3", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parity-wasm" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc878dac00da22f8f61e7af3157988424567ab01d9920b962ef7dcbd7cd865" + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "proptest" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d9cc07f18492d879586c92b485def06bc850da3118075cd45d50e9c95b0e5" +dependencies = [ + "bit-set", + "bitflags", + "byteorder", + "lazy_static", + "num-traits", + "quick-error 2.0.1", + "rand 0.8.4", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", +] + +[[package]] +name = "pwasm-utils" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8ac87af529432d3a4f0e2b3bbf08af49f28f09cc73ed7e551161bdaef5f78d" +dependencies = [ + "byteorder", + "log", + "parity-wasm", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def50a86306165861203e7f84ecffbbdfdea79f0e51039b33de1e952358c47ac" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schemars" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82485a532ef0af18878ad4281f73e58161cdba1db7918176e9294f0ca5498a5" +dependencies = [ + "dyn-clone", + "indexmap", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791c2c848cff1abaeae34fef7e70da5f93171d9eea81ce0fe969a1df627a61a8" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "serde" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b3c34c1690edf8174f5b289a336ab03f568a4460d8c6df75f2f3a692b3bc6a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.134" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784ed1fbfa13fe191077537b0d70ec8ad1e903cfe04831da608aa36457cb653d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edde269018d33d7146dd074e5f7da6fef9b0a957507454c867caa0852c560a9a" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "signature" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f060a7d147e33490ec10da418795238fd7545bba241504d6b31a409f2e6210" +dependencies = [ + "digest", + "rand_core 0.5.1", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "tests" +version = "0.1.0" +dependencies = [ + "casper-dao-contracts", + "casper-dao-utils", + "casper-types", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "tracing" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasmi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad7e265153e1010a73e595eef3e2fd2a1fd644ba4e2dd3af4dd6bd7ec692342" +dependencies = [ + "downcast-rs", + "libc", + "memory_units 0.3.0", + "num-rational 0.2.4", + "num-traits", + "parity-wasm", + "wasmi-validation", +] + +[[package]] +name = "wasmi-validation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea78c597064ba73596099281e2f4cfc019075122a65cdda3205af94f0b264d93" +dependencies = [ + "parity-wasm", +] + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units 0.4.0", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "zeroize" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc222aec311c323c717f56060324f32b82da1ce1dd81d9a09aa6a9030bfe08db" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e8f13fef10b63c06356d65d416b070798ddabcadc10d3ece0c5be9b3c7eddb" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..474d0548 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = [ + "utils", + "macros", + "contracts", + "tests" +] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..cc365062 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +prepare: + rustup target add wasm32-unknown-unknown + +build-contracts: + cargo build --release --target wasm32-unknown-unknown -p casper-dao-contracts + wasm-strip target/wasm32-unknown-unknown/release/reputation_contract.wasm 2>/dev/null | true + +test: build-contracts + cp target/wasm32-unknown-unknown/release/reputation_contract.wasm tests/wasm + cargo test -p tests + +clippy: + cargo clippy --all-targets -- -D warnings -A clippy::bool-assert-comparison + +check-lint: clippy + cargo fmt -- --check + +lint: clippy + cargo fmt + +clean: + cargo clean + rm -rf tests/wasm/*.wasm + +docs: + cargo doc --features test-support --no-deps --open \ No newline at end of file diff --git a/README.md b/README.md index a2ee7da4..b187984a 100644 --- a/README.md +++ b/README.md @@ -1 +1,29 @@ -# dao-contracts \ No newline at end of file +# MVPR DAO for Casper + +Reusable smart contracts for building DAOs on top of Casper. + +Repository contains following modules: +- `contract` provides smart contracts implementation, +- `utils` and `macros` makes writing code easier, +- `tests` contain integration tests, +- `client` implements a JavaScript client for smart contracts interactions. + +## Build contracts +Build `WASM` files. + +```bash +$ make build-contracts +``` + +## Test +Run integration tests. + +```bash +$ make test +``` + +## Docs +Generate `rustdoc`. Opens a new browser window. +```bash +$ make docs +``` diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 00000000..c134ff79 --- /dev/null +++ b/client/.env.example @@ -0,0 +1,8 @@ +WASM_RELEASE_PATH=/your-path/dao-contracts/target/wasm32-unknown-unknown/release +NCTL_USERS_FOLDER_PATH=/your-path/casper-node/utils/nctl/assets/net-1/users + +CHAIN_NAME=casper-net-1 +NODE_ADDRESS=http://localhost:11101/rpc +EVENT_STREAM_ADDRESS=http://localhost:18101/events/main +INSTALL_PAYMENT_AMOUNT=200000000000 +DEPLOY_PAYMENT_AMOUNT=20000000000 diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..79aa6747 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules/ +dist/ +.npmrc +.env +package-lock.json diff --git a/client/README.md b/client/README.md new file mode 100644 index 00000000..9e20f052 --- /dev/null +++ b/client/README.md @@ -0,0 +1,73 @@ +# `dao-contracts-js-client` + +This JavaScript client gives you an easy way to install and interact with the DAO Reputation contract. + +## Installation + +Run this command to install the client: + +```bash +npm i dao-contracts-js-client +``` + +## Usage + +### Install the contract on the network + +```ts +const installDeployHash = await installReputationContract( + CHAIN_NAME, + NODE_ADDRESS, + KEYS, // Key pair used for signing + 200000000000, // Payment amount + "../target/wasm32-unknown-unknown/release/reputation_contract.wasm" // Path to WASM file +); +``` + +### Create an instance to interact with the contract + +```ts +const reputationContract = new ReputationContractJSClient( + http://localhost:11101, // RPC address + "casper-net-1", // Network name + "http://localhost:18101/events/main", // Event stream address + "hash-XXXXXXXXXXXXXXXXXXXXx", // contractPackageHash + "hash-XXXXXXXXXXXXXXXXXXXXx", // contractHash +); +``` + +## API + +### Getters + +Use getter methods to retrieve values: + +```ts +const owner = await reputationContract.getOwner(); +const total_supply = await reputationContract.getTotalSupply(); +``` + +### Deploys + +Use deploys to interact with contract: + +```ts +const mintAmount = "200000000000"; + +const deployHashMint = await reputationContract.mint( + ownerKeys, + ownerKeys.publicKey, + mintAmount, + DEPLOY_PAYMENT_AMOUNT +); +``` + +## Development + +- Set the environment variables in the `.env.js-client` file. Use `.env.js-client.example` as a template +- Install dependencies: `npm i` +- Run the contract with e2e test script: `npm run e2e:reputation` + +## Related Projects + +You can find all the available examples in the [central project repository](https://github.com/casper-network/casper-contracts-js-clients). diff --git a/client/e2e/e2e-reputation.ts b/client/e2e/e2e-reputation.ts new file mode 100644 index 00000000..a026cd04 --- /dev/null +++ b/client/e2e/e2e-reputation.ts @@ -0,0 +1,356 @@ +import { utils } from "casper-js-client-helper"; +import { EventName, EventStream, Keys } from "casper-js-sdk"; + +if (process.env.NODE_ENV !== "ci") { + require("dotenv").config({ path: "./.env", debug: true }); +} + +import { + createInstallReputationContractDeploy, + ReputationContractEventParser, + ReputationContractEvents, + ReputationContractJSClient, +} from "../src"; +import { createRpcClient } from "../src/common/rpc-client"; +import { + getAccountInfo, + getAccountNamedKeyValue, + waitForDeploy, + assert +} from "./utils"; + +const { + NODE_ENV, + CHAIN_NAME, + NODE_ADDRESS, + EVENT_STREAM_ADDRESS, + WASM_RELEASE_PATH, + NCTL_USERS_FOLDER_PATH, + INSTALL_PAYMENT_AMOUNT, + DEPLOY_PAYMENT_AMOUNT, +} = process.env; + +console.log("testing env variables", { + NODE_ENV, + CHAIN_NAME, + NODE_ADDRESS, + EVENT_STREAM_ADDRESS, + WASM_RELEASE_PATH, + NCTL_USERS_FOLDER_PATH, + INSTALL_PAYMENT_AMOUNT, + DEPLOY_PAYMENT_AMOUNT, +}); + +const ownerKeys = Keys.Ed25519.parseKeyFiles( + `${NCTL_USERS_FOLDER_PATH}/user-1/public_key.pem`, + `${NCTL_USERS_FOLDER_PATH}/user-1/secret_key.pem` +); +const recipientKeys = Keys.Ed25519.parseKeyFiles( + `${NCTL_USERS_FOLDER_PATH}/user-2/public_key.pem`, + `${NCTL_USERS_FOLDER_PATH}/user-2/secret_key.pem` +); +const test = async () => { + console.log(`... Try install ...`); + + const installDeploy = createInstallReputationContractDeploy( + CHAIN_NAME, + NODE_ADDRESS, + INSTALL_PAYMENT_AMOUNT, + WASM_RELEASE_PATH + '/reputation_contract.wasm', + ownerKeys + ); + + const installDeployHash = await installDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, installDeployHash); + let accountInfo = await getAccountInfo(NODE_ADDRESS, ownerKeys.publicKey); + const contractPackageHash = await getAccountNamedKeyValue( + accountInfo, + `reputation_contract_package_hash` + ); + if (!contractPackageHash) { + throw Error("Contract not installed correctly!"); + } + + console.log(`... Contract installed successfully.`); + console.log(` - Contract Package Hash: ${contractPackageHash}`); + + /** Register to event stream */ + + const es = new EventStream(EVENT_STREAM_ADDRESS!); + es.subscribe(EventName.DeployProcessed, (event) => { + const parsedEvents = ReputationContractEventParser( + { + contractPackageHash, + eventNames: [ + ReputationContractEvents.AddedToWhitelist, + ReputationContractEvents.Burn, + ReputationContractEvents.Transfer, + ReputationContractEvents.Mint, + ReputationContractEvents.RemovedFromWhitelist, + ReputationContractEvents.OwnerChanged, + ], + }, + event + ); + + if (parsedEvents && parsedEvents.success) { + console.log("*** EVENT START ***"); + console.log(parsedEvents.data); + console.log("*** EVENT END ***"); + } + }); + es.start(); + console.log( + ` - Registered JS event parser to the event stream: ${contractPackageHash}` + ); + + const stateRootHash = await utils.getStateRootHash(NODE_ADDRESS); + const rpcClient = createRpcClient(NODE_ADDRESS); + const res = await rpcClient.fetchStateGetItem( + stateRootHash, + contractPackageHash, + [] + ); + + const contractHash = res.ContractPackage.versions[0].contract_hash; + const contractHashWithHashPrefix = contractHash.replace("contract-", "hash-"); + + // Initialize contract client + const reputationContract = new ReputationContractJSClient( + NODE_ADDRESS, + CHAIN_NAME, + contractHashWithHashPrefix, + contractPackageHash, + EVENT_STREAM_ADDRESS + ); + + console.log(`\n`); + console.log(`... Testing named keys getters ...`); + + const owner = await reputationContract.getOwner(); + console.log(` - Owner: ${owner}`); + assert(owner != null); + + const total_supply = await reputationContract.getTotalSupply(); + console.log(` - Total Supply: ${total_supply}`); + assert(total_supply == '0'); + + const balance = await reputationContract.getBalanceOf(ownerKeys.publicKey); + console.log(` - Balance: ${balance}`); + assert(balance === undefined); + + const stake = await reputationContract.getStakeOf(ownerKeys.publicKey); + console.log(` - Stake: ${stake}`); + assert(stake === undefined); + + const whitelist = await reputationContract.getWhitelistOf( + ownerKeys.publicKey + ); + console.log(` - Whitelist: ${whitelist}`); + assert(whitelist === 'true'); + + console.log(`\n`); + console.log(`... Testing deploys ...`); + + /** MINT DEPLOY */ + + console.log(`\n`); + console.log(" - mint deploy"); + + const mintAmount = "200000000000"; + const mintDeploy = reputationContract.createDeployMint( + ownerKeys.publicKey, + mintAmount, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const mintDeployHash = await mintDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, mintDeployHash); + const totalSupplyEqMint = + mintAmount === (await reputationContract.getTotalSupply()).toString(); + + console.log( + ` - Total supply equals mint: ${totalSupplyEqMint ? "SUCCESS!" : "FAILED!"}` + ); + assert(totalSupplyEqMint); + + /** BURN DEPLOY */ + + console.log(`\n`); + console.log(" - burn deploy"); + + const burnAmount = "100000000000"; + const burnDeploy = reputationContract.createDeployBurn( + ownerKeys.publicKey, + burnAmount, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const burnDeployHash = await burnDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, burnDeployHash); + const totalSupplyEqMintSubtractedByBurn = + (Number(mintAmount) - Number(burnAmount)).toString() === + (await reputationContract.getTotalSupply()).toString(); + + console.log( + ` - Total supply equals mint subtracted by burn: ${ + totalSupplyEqMintSubtractedByBurn ? "SUCCESS!" : "FAILED!" + }` + ); + assert(totalSupplyEqMintSubtractedByBurn); + + /** TRANSFER_FROM DEPLOY */ + + console.log(`\n`); + console.log(" - transfer_from deploy"); + + const transferAmount = "10000000"; + const transferDeploy = reputationContract.createDeployTransferFrom( + ownerKeys.publicKey, + recipientKeys.publicKey, + transferAmount, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const transferDeployHash = await transferDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, transferDeployHash); + // `balance` named-key returns a simple string type while `total_supply` named-key returns a Big number representation, + // both of them conceptually represent a token amount. I would like to suggest to have + // balance return the same representation as total so they are consistent, would be easier to consume by users + const recipientBalanceEqTransferAmount = + transferAmount === + (await reputationContract.getBalanceOf(recipientKeys.publicKey)); + + console.log( + ` - Recipient balance received transfer: ${ + recipientBalanceEqTransferAmount ? "SUCCESS!" : "FAILED!" + }` + ); + assert(recipientBalanceEqTransferAmount); + + /** ADD_TO_WHITELIST DEPLOY */ + + console.log(`\n`); + console.log(" - add_to_whitelist deploy"); + + const whitelistAddDeploy = reputationContract.createDeployAddToWhitelist( + recipientKeys.publicKey, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const whitelistAddDeployHash = await whitelistAddDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, whitelistAddDeployHash); + const recipientAddedToTheWhitelist = + "true" === + (await reputationContract.getWhitelistOf(recipientKeys.publicKey)); + + console.log( + ` - Recipient is added to the whitelist: ${ + recipientAddedToTheWhitelist ? "SUCCESS!" : "FAILED!" + }` + ); + assert(recipientAddedToTheWhitelist); + + /** REMOVE_FROM_WHITELIST DEPLOY */ + + console.log(`\n`); + console.log(" - remove_from_whitelist deploy"); + + const whitelistRemoveDeploy = + reputationContract.createDeployRemoveFromWhitelist( + recipientKeys.publicKey, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const whitelistRemoveDeployHash = await whitelistRemoveDeploy.send( + NODE_ADDRESS + ); + await waitForDeploy(NODE_ADDRESS, whitelistRemoveDeployHash); + const recipientRemovedFromTheWhitelist = + "false" === + (await reputationContract.getWhitelistOf(recipientKeys.publicKey)); + + console.log( + ` - Recipient is removed from the whitelist: ${ + recipientRemovedFromTheWhitelist ? "SUCCESS!" : "FAILED!" + }` + ); + assert(recipientRemovedFromTheWhitelist); + + /** STAKE DEPLOY */ + + console.log(`\n`); + console.log(" - stake deploy"); + + const stakeAmount = "10000000"; + const stakeDeploy = reputationContract.createDeployStake( + ownerKeys.publicKey, + stakeAmount, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const stakeDeployHash = await stakeDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, stakeDeployHash); + const stakeAmountWasStaked = + stakeAmount === (await reputationContract.getStakeOf(ownerKeys.publicKey)); + + console.log( + ` - Requested amount was staked: ${ + stakeAmountWasStaked ? "SUCCESS!" : "FAILED!" + }` + ); + assert(stakeAmountWasStaked); + + /** UNSTAKE DEPLOY */ + + console.log(`\n`); + console.log(" - unstake deploy"); + + const unstakeDeploy = reputationContract.createDeployUnstake( + ownerKeys.publicKey, + stakeAmount, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const unstakeDeployHash = await unstakeDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, unstakeDeployHash); + const stakeAmountWasUnstaked = + "0" === (await reputationContract.getStakeOf(ownerKeys.publicKey)); + + console.log( + ` - Requested amount was unstaked: ${ + stakeAmountWasUnstaked ? "SUCCESS!" : "FAILED!" + }` + ); + assert(stakeAmountWasUnstaked); + + /** CHANGE_OWNERSHIP DEPLOY */ + + console.log(`\n`); + console.log(" - change_ownership deploy"); + + const changeOwnerDeploy = reputationContract.createDeployChangeOwnership( + recipientKeys.publicKey, + DEPLOY_PAYMENT_AMOUNT, + ownerKeys + ); + const changeOwnerDeployHash = await changeOwnerDeploy.send(NODE_ADDRESS); + await waitForDeploy(NODE_ADDRESS, changeOwnerDeployHash); + // It's not dev-friendly but I couldn't find any better way to read the value from this CLType, + // that is a result of parsing `ts-results` inside the `casper-js-sdk`? Is there any cleaner way to fix it? + const newOwner = (await reputationContract.getOwner())["val"].data.data; + const ownerChangedToRecipient = + Buffer.from(recipientKeys.publicKey.toAccountHash()).toString("hex") === + Buffer.from(newOwner).toString("hex"); + + console.log( + ` - Owner changed to recipient: ${ + ownerChangedToRecipient ? "SUCCESS!" : "FAILED!" + }` + ); + assert(ownerChangedToRecipient); +}; + +test().then(() => { + process.exit(0); +}); diff --git a/client/e2e/utils.ts b/client/e2e/utils.ts new file mode 100644 index 00000000..81d0d940 --- /dev/null +++ b/client/e2e/utils.ts @@ -0,0 +1,100 @@ +import { + CasperClient, + CasperServiceByJsonRPC, + CLPublicKey, +} from "casper-js-sdk"; + +export const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +export const assert = (val: any) => { + if (!Boolean(val)) { + throw Error('Test failed!') + } +} + +export const getDeploy = async (NODE_ADDRESS: string, deployHash: string) => { + const client = new CasperClient(NODE_ADDRESS); + let i = 300; + while (i != 0) { + const [deploy, raw] = await client.getDeploy(deployHash); + + if (raw.execution_results.length !== 0) { + // @ts-ignore + if (raw.execution_results[0].result.Success) { + return deploy; + } else { + // @ts-ignore + throw Error( + "Deploy execution: " + + // @ts-ignore + raw.execution_results[0].result.Failure.error_message + ); + } + } else { + i--; + await sleep(3000); + continue; + } + } + throw Error("Timeout after " + i + "s. Something's wrong"); +}; + +export const waitForDeploy = async ( + NODE_ADDRESS: string, + deployHash: string +) => { + console.log( + `... Contract deploy is pending, waiting for next block finalisation (deployHash: ${deployHash}) ...` + ); + + const deploy = await getDeploy(NODE_ADDRESS, deployHash); + return deploy; +}; + +export const getAccountInfo: any = async ( + nodeAddress: string, + publicKey: CLPublicKey +) => { + const client = new CasperServiceByJsonRPC(nodeAddress); + const stateRootHash = await client.getStateRootHash(); + const accountHash = publicKey.toAccountHashStr(); + const blockState = await client.getBlockState(stateRootHash, accountHash, []); + return blockState.Account; +}; + +/** + * Returns a value under an on-chain account's storage. + * @param accountInfo - On-chain account's info. + * @param namedKey - A named key associated with an on-chain account. + */ +export const getAccountNamedKeyValue = (accountInfo: any, namedKey: string) => { + const found = accountInfo.namedKeys.find((i: any) => i.name === namedKey); + if (found) { + return found.key; + } + return undefined; +}; + +export const encodeAccountHashStrAsKey = (accountHashStr: string) => { + const str = accountHashStr.startsWith("account-hash-") + ? accountHashStr.replace("account-hash-", "00") + : "00" + accountHashStr; + return Buffer.from(str, "hex").toString("base64"); +}; + +export const createDictionaryGetter = async ( + contractClient: any, + path: string, + account: CLPublicKey +) => { + const key = encodeAccountHashStrAsKey(account.toAccountHashStr()); + try { + const result = await contractClient.queryContractDictionary(path, key); + return result.value().toString(); + } catch (err) { + if (err.message.includes("ValueNotFound")) return; + else throw err; + } +}; diff --git a/client/package.json b/client/package.json new file mode 100644 index 00000000..93e4c7a0 --- /dev/null +++ b/client/package.json @@ -0,0 +1,32 @@ +{ + "name": "dao-contracts-js-client", + "version": "0.1.0", + "description": "This JavaScript client gives you an easy way to install and interact with the DAO smart contract.", + "author": "Piotr Witek ", + "homepage": "https://make.services", + "license": "ISC", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "@open-rpc/client-js": "1.7.1", + "casper-js-client-helper": "1.0.0", + "casper-js-sdk": "2.8.0" + }, + "devDependencies": { + "dotenv": "16.0.0", + "ts-node": "10.5.0", + "typescript": "4.5.5" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "dist": "npm run clean && npm run build", + "clean": "rm -rf dist/", + "build": "tsc -p tsconfig.build.json", + "e2e:reputation": "ts-node ./e2e/e2e-reputation.ts" + } +} diff --git a/client/src/common/constants.ts b/client/src/common/constants.ts new file mode 100644 index 00000000..0efa83d5 --- /dev/null +++ b/client/src/common/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_TTL = 1800000; diff --git a/client/src/common/rpc-client.ts b/client/src/common/rpc-client.ts new file mode 100644 index 00000000..cd024160 --- /dev/null +++ b/client/src/common/rpc-client.ts @@ -0,0 +1,33 @@ +import { RequestManager, HTTPTransport, Client } from "@open-rpc/client-js"; +import { StoredValue } from "casper-js-sdk/dist/lib/StoredValue"; + +export const createRpcClient = (nodeAddress: string) => { + const transport = new HTTPTransport(nodeAddress); + const requestManager = new RequestManager([transport]); + const rpcClient = new Client(requestManager); + + const fetchStateGetItem = async ( + stateRootHash: string, + key: string, + path: string[] = [] + ) => { + return await rpcClient + .request({ + method: "state_get_item", + params: { + state_root_hash: stateRootHash, + key: key, + path, + }, + }) + .then((res) => + res.error != null ? res.error : (res.stored_value as StoredValue) + ).catch(err => { + console.error(err); + }); + }; + + return { + fetchStateGetItem, + }; +}; diff --git a/client/src/index.ts b/client/src/index.ts new file mode 100644 index 00000000..47308015 --- /dev/null +++ b/client/src/index.ts @@ -0,0 +1,13 @@ +import { + ReputationContractEvents, + ReputationContractEventParser, +} from "./reputation/events"; +import { ReputationContractJSClient } from "./reputation/client"; +import { createInstallReputationContractDeploy } from "./reputation/install"; + +export { + createInstallReputationContractDeploy, + ReputationContractJSClient, + ReputationContractEvents, + ReputationContractEventParser, +}; diff --git a/client/src/reputation/client.ts b/client/src/reputation/client.ts new file mode 100644 index 00000000..a93a0b20 --- /dev/null +++ b/client/src/reputation/client.ts @@ -0,0 +1,335 @@ +import { CasperContractClient } from "casper-js-client-helper"; +import { createRecipientAddress } from "casper-js-client-helper/dist/helpers/lib"; +import { + CasperClient, + CLPublicKey, + CLValueBuilder, + Contracts, + Keys, + RuntimeArgs, +} from "casper-js-sdk"; + +import { createDictionaryGetter } from "../../e2e/utils"; +import { DEFAULT_TTL } from "../common/constants"; +import { createRpcClient } from "../common/rpc-client"; + +export class ReputationContractJSClient extends CasperContractClient { + protected rpcClient: ReturnType; + protected contractClient: Contracts.Contract; + + constructor( + nodeAddress: string, + chainName: string, + contractHash: string, + contractPackageHash: string, + eventStreamAddress?: string + ) { + super(nodeAddress, chainName, eventStreamAddress); + this.contractClient = new Contracts.Contract(new CasperClient(nodeAddress)); + this.contractClient.setContractHash(contractHash, contractPackageHash); + this.rpcClient = createRpcClient(nodeAddress); + } + + /** + * Returns owner + */ + public async getOwner() { + return this.contractClient.queryContractData(["owner"]); + } + + /** + * Returns total supply + */ + public async getTotalSupply() { + return this.contractClient.queryContractData(["total_supply"]); + } + + /** + * Returns balance of the specified account + */ + public async getBalanceOf(account: CLPublicKey) { + return await createDictionaryGetter( + this.contractClient, + "balances", + account + ); + } + + /** + * Returns whitelist status of the specified account + */ + public async getWhitelistOf(account: CLPublicKey) { + return await createDictionaryGetter( + this.contractClient, + "whitelist", + account + ); + } + + /** + * Returns stake of the specified account + */ + public async getStakeOf(account: CLPublicKey) { + return await createDictionaryGetter(this.contractClient, "stakes", account); + } + + /** + * Transfer an amount of tokens from address to address + * + * @param owner Owner address. + * @param recipient Recipient address. + * @param transferAmount Amount of tokens that will be transfered. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployTransferFrom( + owner: CLPublicKey, + recipient: CLPublicKey, + transferAmount: string, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + owner: createRecipientAddress(owner), + recipient: createRecipientAddress(recipient), + amount: CLValueBuilder.u256(transferAmount), + }); + + const deployHash = this.contractClient.callEntrypoint( + "transfer_from", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Mint an amount of tokens + * + * @param recipient Recipient address. + * @param amount Amount of tokens that will be minted. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployMint( + recipient: CLPublicKey, + amount: string, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + recipient: createRecipientAddress(recipient), + amount: CLValueBuilder.u256(amount), + }); + + const deployHash = this.contractClient.callEntrypoint( + "mint", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + [keys] + ); + + return deployHash; + } + + /** + * Burn an amount of tokens + * + * @param owner Owner address. + * @param amount Amount of tokens that will be burned. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployBurn( + owner: CLPublicKey, + amount: string, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + owner: createRecipientAddress(owner), + amount: CLValueBuilder.u256(amount), + }); + + const deployHash = this.contractClient.callEntrypoint( + "burn", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Change contract owner + * + * @param owner New owner address. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployChangeOwnership( + owner: CLPublicKey, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + owner: createRecipientAddress(owner), + }); + + const deployHash = this.contractClient.callEntrypoint( + "change_ownership", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Add to whitelist + * + * @param address Recipient address. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployAddToWhitelist( + address: CLPublicKey, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + address: createRecipientAddress(address), + }); + + const deployHash = this.contractClient.callEntrypoint( + "add_to_whitelist", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Remove from whitelist + * + * @param address Recipient address. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployRemoveFromWhitelist( + address: CLPublicKey, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + address: createRecipientAddress(address), + }); + + const deployHash = this.contractClient.callEntrypoint( + "remove_from_whitelist", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Stake an amount of tokens + * + * @param address Recipient address. + * @param amount Amount of tokens that will be staked. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployStake( + address: CLPublicKey, + amount: string, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + address: createRecipientAddress(address), + amount: CLValueBuilder.u256(amount), + }); + + const deployHash = this.contractClient.callEntrypoint( + "stake", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deployHash; + } + + /** + * Unstake an amount of tokens + * + * @param address Recipient address. + * @param amount Amount of tokens that will be unstaked. + * @param paymentAmount Amount that will be used to pay the transaction. + * @param keys AsymmetricKey that will be used to sign the transaction. + * + * @returns Deploy hash. + */ + public createDeployUnstake( + address: CLPublicKey, + amount: string, + paymentAmount: string, + keys: Keys.AsymmetricKey = undefined + ) { + const runtimeArgs = RuntimeArgs.fromMap({ + address: createRecipientAddress(address), + amount: CLValueBuilder.u256(amount), + }); + + const deploy = this.contractClient.callEntrypoint( + "unstake", + runtimeArgs, + keys.publicKey, + this.chainName, + paymentAmount, + keys && [keys] + ); + + return deploy; + } + + // EOF +} diff --git a/client/src/reputation/events.ts b/client/src/reputation/events.ts new file mode 100644 index 00000000..5301717a --- /dev/null +++ b/client/src/reputation/events.ts @@ -0,0 +1,59 @@ +import { CLMap, CLValueBuilder, CLValueParsers } from "casper-js-sdk"; + +export enum ReputationContractEvents { + Transfer = "transfer", + Mint = "mint", + Burn = "burn", + OwnerChanged = "owner_changed", + AddedToWhitelist = "added_to_whitelist", + RemovedFromWhitelist = "removed_from_whitelist", + TokensStaked = "tokens_staked", + TokensUnstaked = "tokens_unstaked", +} + +export const ReputationContractEventParser = ( + { + contractPackageHash, + eventNames, + }: { contractPackageHash: string; eventNames: ReputationContractEvents[] }, + value: any +) => { + if (value.body.DeployProcessed.execution_result.Success) { + const { transforms } = + value.body.DeployProcessed.execution_result.Success.effect; + + const eventsData = transforms.reduce((acc: any, val: any) => { + if ( + val.transform.hasOwnProperty("WriteCLValue") && + typeof val.transform.WriteCLValue.parsed === "object" && + val.transform.WriteCLValue.parsed !== null + ) { + const maybeCLValue = CLValueParsers.fromJSON( + val.transform.WriteCLValue + ); + const clValue = maybeCLValue.unwrap(); + if (clValue && clValue instanceof CLMap) { + const hash = clValue.get( + CLValueBuilder.string("contract_package_hash") + ); + const event = clValue.get(CLValueBuilder.string("event_type")); + if ( + hash && + // NOTE: Calling toLowerCase() because current JS-SDK doesn't support checksumed hashes and returns all lower case value + // Remove it after updating SDK + hash.value() === contractPackageHash.slice(5).toLowerCase() && + event && + eventNames.includes(event.value()) + ) { + acc = [...acc, { name: event.value(), clValue }]; + } + } + } + return acc; + }, []); + + return { error: null, success: !!eventsData.length, data: eventsData }; + } + + return null; +}; diff --git a/client/src/reputation/install.ts b/client/src/reputation/install.ts new file mode 100644 index 00000000..f6432407 --- /dev/null +++ b/client/src/reputation/install.ts @@ -0,0 +1,39 @@ +import { CasperClient, Contracts, Keys, RuntimeArgs } from "casper-js-sdk"; +import * as fs from "fs"; + +const getBinary = (pathToBinary: string) => { + return new Uint8Array(fs.readFileSync(pathToBinary, null).buffer); +}; + +/** + * Installs the contract. + * + * @param chainName Name of the chain + * @param nodeAddress Url of node + * @param paymentAmount The payment amount that will be used to install the contract. + * @param wasmPath Path to the WASM file that will be installed. + * @param keys AsymmetricKey that will be used to install the contract. + * + * @returns Installation deploy hash. + */ +export function createInstallReputationContractDeploy( + chainName: string, + nodeAddress: string, + paymentAmount: string, + wasmPath: string, + keys?: Keys.AsymmetricKey +) { + const contractClient = new Contracts.Contract(new CasperClient(nodeAddress)); + const runtimeArgs = RuntimeArgs.fromNamedArgs([]); + + const deploy = contractClient.install( + getBinary(wasmPath), + runtimeArgs, + paymentAmount, + keys.publicKey, + chainName, + keys && [keys] + ); + + return deploy; +} diff --git a/client/test.ts b/client/test.ts new file mode 100644 index 00000000..53ee949b --- /dev/null +++ b/client/test.ts @@ -0,0 +1,76 @@ +import { config } from "dotenv"; +config({ path: "./.env.js-client" }); +const { + CHAIN_NAME, + NODE_ADDRESS, + EVENT_STREAM_ADDRESS, + NCTL_USERS_FOLDER_PATH, + INSTALL_PAYMENT_AMOUNT, + DEPLOY_PAYMENT_AMOUNT, +} = process.env; +import { Ok, Err, Option, Some, None } from 'ts-results'; + +import { utils } from "casper-js-client-helper"; +import { + encodeAccountHashStrAsKey, + getAccountInfo, + getAccountNamedKeyValue, + getDeploy, + waitForDeploy, +} from "./e2e/utils"; +import { CasperClient, Keys } from "casper-js-sdk"; +import { ReputationContractEvents, ReputationContractJSClient } from "./src"; +import { createRpcClient } from "./src/common/rpc-client"; +import { decodeBase64, encodeBase64 } from "tweetnacl-ts"; + +const ownerKeys = Keys.Ed25519.parseKeyFiles( + `${NCTL_USERS_FOLDER_PATH}/user-1/public_key.pem`, + `${NCTL_USERS_FOLDER_PATH}/user-1/secret_key.pem` +); +const recipientKeys = Keys.Ed25519.parseKeyFiles( + `${NCTL_USERS_FOLDER_PATH}/user-2/public_key.pem`, + `${NCTL_USERS_FOLDER_PATH}/user-2/secret_key.pem` +); + +const casperClient = new CasperClient(NODE_ADDRESS).nodeClient; +const rpcClient = createRpcClient(NODE_ADDRESS); + +const run = async () => { + const stateRootHash = await utils.getStateRootHash(NODE_ADDRESS); + let accountInfo = await getAccountInfo(NODE_ADDRESS!, ownerKeys.publicKey); + const contractPackageHash = await getAccountNamedKeyValue( + accountInfo, + `reputation_contract_package_hash` + ); + const res = await rpcClient.fetchStateGetItem( + stateRootHash, + contractPackageHash, + [] + ); + + const ch = res.ContractPackage.versions[0].contract_hash; + const contractHash = ch.replace("contract-", "hash-"); + + const reputationContract = new ReputationContractJSClient( + NODE_ADDRESS, + CHAIN_NAME, + contractHash, + contractPackageHash, + EVENT_STREAM_ADDRESS + ); + const mintAmount = "200000000000"; + + const ownerChangedToRecipient = + Buffer.from(recipientKeys.publicKey.toAccountHash()).toString('hex') === Buffer.from((await reputationContract.getOwner())['val'].data.data).toString('hex'); + + console.log( + ` - Owner changed to recipient: ${ + ownerChangedToRecipient ? "Success" : "Failed" + }`, + '\n' + ); + + return; +}; + +run(); diff --git a/client/tsconfig.build.json b/client/tsconfig.build.json new file mode 100644 index 00000000..909f95a5 --- /dev/null +++ b/client/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "outDir": "./dist" + }, + + "include": [ + "src/**/*" + ] +} + diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 00000000..7c033177 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "noImplicitAny": false, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitUseStrict": true, + "removeComments": true, + "declaration": true, + "target": "es5", + "lib": ["es2015", "dom"], + "module": "commonjs", + "sourceMap": true, + "resolveJsonModule": true, + "typeRoots": ["node_modules/@types"], + "esModuleInterop": true, + "moduleResolution": "node", + "skipLibCheck": true + }, + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml new file mode 100644 index 00000000..148e62c1 --- /dev/null +++ b/contracts/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "casper-dao-contracts" +version = "0.1.0" +edition = "2018" + +[dependencies] +casper-contract = "1.4.3" +casper-types = "1.4.6" +casper-dao-utils = { path = "../utils" } + +[[bin]] +name = "reputation_contract" +path = "bin/reputation_contract.rs" +bench = false +doctest = false +test = false +doc = false + +[profile.release] +codegen-units = 1 +lto = true + +[features] +default = [ "casper-contract/default", "casper-contract/std", "casper-contract/test-support" ] +test-support = [ "casper-dao-utils/test-support", "casper-contract/test-support" ] \ No newline at end of file diff --git a/contracts/bin/reputation_contract.rs b/contracts/bin/reputation_contract.rs new file mode 100644 index 00000000..0a7c603a --- /dev/null +++ b/contracts/bin/reputation_contract.rs @@ -0,0 +1,70 @@ +#![no_main] + +use casper_contract::contract_api::runtime; +use casper_dao_contracts::{ReputationContract, ReputationContractInterface}; +use casper_dao_utils::{consts, Address}; +use casper_types::U256; + +#[no_mangle] +fn call() { + ReputationContract::install(); +} + +#[no_mangle] +fn init() { + ReputationContract::default().init(); +} + +#[no_mangle] +fn mint() { + let recipient: Address = runtime::get_named_arg(consts::PARAM_RECIPIENT); + let amount: U256 = runtime::get_named_arg(consts::PARAM_AMOUNT); + ReputationContract::default().mint(recipient, amount); +} + +#[no_mangle] +fn burn() { + let owner: Address = runtime::get_named_arg(consts::PARAM_OWNER); + let amount: U256 = runtime::get_named_arg(consts::PARAM_AMOUNT); + ReputationContract::default().burn(owner, amount); +} + +#[no_mangle] +fn transfer_from() { + let owner: Address = runtime::get_named_arg(consts::PARAM_OWNER); + let recipient: Address = runtime::get_named_arg(consts::PARAM_RECIPIENT); + let amount: U256 = runtime::get_named_arg(consts::PARAM_AMOUNT); + ReputationContract::default().transfer_from(owner, recipient, amount); +} + +#[no_mangle] +fn change_ownership() { + let owner: Address = runtime::get_named_arg(consts::PARAM_OWNER); + ReputationContract::default().change_ownership(owner); +} + +#[no_mangle] +fn add_to_whitelist() { + let address: Address = runtime::get_named_arg(consts::PARAM_ADDRESS); + ReputationContract::default().add_to_whitelist(address); +} + +#[no_mangle] +fn remove_from_whitelist() { + let address: Address = runtime::get_named_arg(consts::PARAM_ADDRESS); + ReputationContract::default().remove_from_whitelist(address); +} + +#[no_mangle] +fn stake() { + let address: Address = runtime::get_named_arg(consts::PARAM_ADDRESS); + let amount: U256 = runtime::get_named_arg(consts::PARAM_AMOUNT); + ReputationContract::default().stake(address, amount); +} + +#[no_mangle] +fn unstake() { + let address: Address = runtime::get_named_arg(consts::PARAM_ADDRESS); + let amount: U256 = runtime::get_named_arg(consts::PARAM_AMOUNT); + ReputationContract::default().unstake(address, amount); +} diff --git a/contracts/resources/reputation_contract_schema.yaml b/contracts/resources/reputation_contract_schema.yaml new file mode 100644 index 00000000..51ce1896 --- /dev/null +++ b/contracts/resources/reputation_contract_schema.yaml @@ -0,0 +1,132 @@ +name: "reputation_contract" + +entry_points: + - name: "mint" + arguments: + - name: "recipient" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + + - name: "burn" + arguments: + - name: "owner" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + + - name: "transfer_from" + arguments: + - name: "owner" + cl_type: "Address" + - name: "recipient" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + + - name: "change_ownership" + arguments: + - name: "owner" + cl_type: "Address" + + - name: "add_to_whitelist" + arguments: + - name: "address" + cl_type: "Address" + + - name: "remove_from_whitelist" + arguments: + - name: "address" + cl_type: "Address" + + - name: "stake" + arguments: + - name: "address" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + + - name: "unstake" + arguments: + - name: "address" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + +events_config: + dictionary_named_key: "events" + length_named_key: "events_length" + +events: + - name: "Transfer" + fields: + - name: "from" + cl_type: "Address" + - name: "to" + cl_type: "Address" + - name: "value" + cl_type: "U256" + + - name: "Mint" + fields: + - name: "recipient" + cl_type: "Address" + - name: "value" + cl_type: "U256" + + - name: "Burn" + fields: + - name: "owner" + cl_type: "Address" + - name: "value" + cl_type: "U256" + + - name: "OwnerChanged" + fields: + - name: "new_owner" + cl_type: "Address" + + - name: "AddedToWhitelist" + fields: + - name: "address" + cl_type: "Address" + + - name: "RemovedFromWhitelist" + fields: + - name: "address" + cl_type: "Address" + + - name: "TokensStaked" + fields: + - name: "address" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + + - name: "TokensUnstaked" + fields: + - name: "address" + cl_type: "Address" + - name: "amount" + cl_type: "U256" + +named_keys: + - name: "owner" + named_key: "owner" + cl_type: "Option
" + + - name: "total_supply" + named_key: "total_supply" + cl_type: "U256" + + - name: "balances" + named_key: "balances" + cl_type: "Mapping" + + - name: "whitelist" + named_key: "whitelist" + cl_type: "Mapping" + + - name: "stakes" + named_key: "stakes" + cl_type: "Mapping" diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs new file mode 100644 index 00000000..7b808c00 --- /dev/null +++ b/contracts/src/lib.rs @@ -0,0 +1,6 @@ +mod reputation; + +pub use reputation::{ReputationContract, ReputationContractCaller, ReputationContractInterface}; + +#[cfg(feature = "test-support")] +pub use reputation::ReputationContractTest; diff --git a/contracts/src/reputation.rs b/contracts/src/reputation.rs new file mode 100644 index 00000000..a7c5ac37 --- /dev/null +++ b/contracts/src/reputation.rs @@ -0,0 +1,530 @@ +use std::collections::BTreeSet; + +use casper_contract::{ + contract_api::{runtime, storage}, + unwrap_or_revert::UnwrapOrRevert, +}; +use casper_dao_utils::{ + casper_env::{caller, init_events}, + consts, + owner::Owner, + staking::TokenWithStaking, + whitelist::Whitelist, + Address, +}; +use casper_types::{ + contracts::NamedKeys, runtime_args, CLTyped, ContractPackageHash, EntryPoint, EntryPointAccess, + EntryPointType, EntryPoints, Group, RuntimeArgs, URef, U256, +}; + +/// Interface of the Reputation Contract. +/// +/// It should be implemented by [`ReputationContract`], [`ReputationContractCaller`] +/// and [`ReputationContractTest`]. +pub trait ReputationContractInterface { + /// Constructor method. + /// + /// It initializes contract elements: + /// * Events dictionary. + /// * Named keys of [`TokenWithStaking`], [`Owner`] and [`Whitelist`]. + /// * Set [`caller`] as the owner of the contract. + /// * Add [`caller`] to the whitelist. + /// + /// It emits [`OwnerChanged`](casper_dao_utils::owner::events::OwnerChanged), + /// [`AddedToWhitelist`](casper_dao_utils::whitelist::events::AddedToWhitelist) events. + fn init(&mut self); + + /// Mint new tokens. Add `amount` of new tokens to the balance of the `recipient` and + /// increment the total supply. Only whitelisted addresses are permited to call this method. + /// + /// It throws [`NotWhitelisted`](casper_dao_utils::Error::NotWhitelisted) if caller + /// is not whitelisted. + /// + /// It emits [`Mint`](casper_dao_utils::token::events::Mint) event. + fn mint(&mut self, recipient: Address, amount: U256); + + /// Burn existing tokens. Remove `amount` of existing tokens from the balance of the `owner` + /// and decrement the total supply. Only whitelisted addresses are permited to call this + /// method. + /// + /// It throws [`NotWhitelisted`](casper_dao_utils::Error::NotWhitelisted) if caller + /// is not whitelisted. + /// + /// It emits [`Burn`](casper_dao_utils::token::events::Burn) event. + fn burn(&mut self, owner: Address, amount: U256); + + /// Transfer `amount` of tokens from `owner` to `recipient`. Only whitelisted addresses are + /// permited to call this method. + /// + /// It throws [`NotWhitelisted`](casper_dao_utils::Error::NotWhitelisted) if caller + /// is not whitelisted. + /// + /// It throws [`InsufficientBalance`](casper_dao_utils::Error::InsufficientBalance) + /// if `recipient`'s balance is less then `amount`. + /// + /// It emits [`Transfer`](casper_dao_utils::token::events::Transfer) event. + fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256); + + /// Change ownership of the contract. Transfer the ownership to the `owner`. Only current owner + /// is permited to call this method. + /// + /// It throws [`NotAnOwner`](casper_dao_utils::Error::NotAnOwner) if caller + /// is not the current owner. + /// + /// It emits [`OwnerChanged`](casper_dao_utils::owner::events::OwnerChanged), + /// [`AddedToWhitelist`](casper_dao_utils::whitelist::events::AddedToWhitelist) events. + fn change_ownership(&mut self, owner: Address); + + /// Add new address to the whitelist. + /// + /// It throws [`NotAnOwner`](casper_dao_utils::Error::NotAnOwner) if caller + /// is not the current owner. + /// + /// It emits [`AddedToWhitelist`](casper_dao_utils::whitelist::events::AddedToWhitelist) event. + fn add_to_whitelist(&mut self, address: Address); + + /// Remove address from the whitelist. + /// + /// It throws [`NotAnOwner`](casper_dao_utils::Error::NotAnOwner) if caller + /// is not the current owner. + /// + /// It emits [`RemovedFromWhitelist`](casper_dao_utils::whitelist::events::RemovedFromWhitelist) + /// event. + fn remove_from_whitelist(&mut self, address: Address); + + /// Stake `amount` of tokens for the `address`. It decrements `address`'s balance by `amount`. + /// + /// It throws [`NotAnOwner`](casper_dao_utils::Error::NotAnOwner) if caller + /// is not the current owner. + /// + /// It throws [`InsufficientBalance`](casper_dao_utils::Error::InsufficientBalance) + /// if `address`'s balance is less then `amount`. + /// + /// It emits [`TokensStaked`](casper_dao_utils::staking::events::TokensStaked) + /// event. + fn stake(&mut self, address: Address, amount: U256); + + /// Unstake `amount` of tokens for the `address`. It increments `address`'s balance by + /// `amount`. + /// + /// It throws [`NotAnOwner`](casper_dao_utils::Error::NotAnOwner) if caller + /// is not the current owner. + /// + /// It throws [`InsufficientBalance`](casper_dao_utils::Error::InsufficientBalance) + /// if `address`'s staked amount is less then `amount`. + /// + /// It emits [`TokensUnstaked`](casper_dao_utils::staking::events::TokensUnstaked) + /// event. + fn unstake(&mut self, address: Address, amount: U256); +} + +/// Implementation of the Reputation Contract. See [`ReputationContractInterface`]. +#[derive(Default)] +pub struct ReputationContract { + pub token: TokenWithStaking, + pub owner: Owner, + pub whitelist: Whitelist, +} + +impl ReputationContractInterface for ReputationContract { + fn init(&mut self) { + init_events(); + let deployer = caller(); + self.owner.init(deployer); + self.whitelist.init(); + self.whitelist.add_to_whitelist(deployer); + self.token.init(); + } + + fn mint(&mut self, recipient: Address, amount: U256) { + self.whitelist.ensure_whitelisted(); + self.token.mint(recipient, amount); + } + + fn burn(&mut self, owner: Address, amount: U256) { + self.whitelist.ensure_whitelisted(); + self.token.burn(owner, amount); + } + + fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256) { + self.whitelist.ensure_whitelisted(); + self.token.raw_transfer(owner, recipient, amount); + } + + fn change_ownership(&mut self, owner: Address) { + self.owner.ensure_owner(); + self.owner.change_ownership(owner); + self.whitelist.add_to_whitelist(owner); + } + + fn add_to_whitelist(&mut self, address: Address) { + self.owner.ensure_owner(); + self.whitelist.add_to_whitelist(address); + } + + fn remove_from_whitelist(&mut self, address: Address) { + self.owner.ensure_owner(); + self.whitelist.remove_from_whitelist(address); + } + + fn stake(&mut self, address: Address, amount: U256) { + self.whitelist.ensure_whitelisted(); + self.token.stake(address, amount); + } + + fn unstake(&mut self, address: Address, amount: U256) { + self.whitelist.ensure_whitelisted(); + self.token.unstake(address, amount); + } +} + +impl ReputationContract { + pub fn install() { + // Create a new contract package hash for the contract. + let (contract_package_hash, _) = storage::create_contract_package_at_hash(); + runtime::put_key( + "reputation_contract_package_hash", + contract_package_hash.into(), + ); + + let init_access: URef = storage::create_contract_user_group( + contract_package_hash, + "init", + 1, + Default::default(), + ) + .unwrap_or_revert() + .pop() + .unwrap_or_revert(); + + storage::add_contract_version( + contract_package_hash, + ReputationContract::entry_points(), + NamedKeys::new(), + ); + + // Call contrustor method. + let mut contract_instance = ReputationContractCaller::at(contract_package_hash); + contract_instance.init(); + + // Revoke access to init. + let mut urefs = BTreeSet::new(); + urefs.insert(init_access); + storage::remove_contract_user_group_urefs(contract_package_hash, "init", urefs) + .unwrap_or_revert(); + } + + pub fn entry_points() -> EntryPoints { + let mut entry_points = EntryPoints::new(); + entry_points.add_entry_point(EntryPoint::new( + "init", + vec![], + <()>::cl_type(), + EntryPointAccess::Groups(vec![Group::new("init")]), + EntryPointType::Contract, + )); + + entry_points.add_entry_point(casper_dao_utils::owner::entry_points::change_ownership()); + entry_points.add_entry_point(casper_dao_utils::whitelist::entry_points::add_to_whitelist()); + entry_points + .add_entry_point(casper_dao_utils::whitelist::entry_points::remove_from_whitelist()); + entry_points.add_entry_point(casper_dao_utils::staking::entry_points::mint()); + entry_points.add_entry_point(casper_dao_utils::staking::entry_points::burn()); + entry_points.add_entry_point(casper_dao_utils::staking::entry_points::transfer_from()); + entry_points.add_entry_point(casper_dao_utils::staking::entry_points::stake()); + entry_points.add_entry_point(casper_dao_utils::staking::entry_points::unstake()); + + entry_points + } +} + +/// Implementation of the Reputation Contract Caller. See [`ReputationContractInterface`]. +pub struct ReputationContractCaller { + contract_package_hash: ContractPackageHash, +} + +impl ReputationContractCaller { + pub fn at(contract_package_hash: ContractPackageHash) -> Self { + ReputationContractCaller { + contract_package_hash, + } + } +} + +impl ReputationContractInterface for ReputationContractCaller { + fn init(&mut self) { + let _: () = runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_INIT, + runtime_args! {}, + ); + } + + fn mint(&mut self, recipient: Address, amount: U256) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_MINT, + runtime_args! { + consts::PARAM_RECIPIENT => recipient, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn burn(&mut self, owner: Address, amount: U256) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_BURN, + runtime_args! { + consts::PARAM_OWNER => owner, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_TRANSFER_FROM, + runtime_args! { + consts::PARAM_OWNER => owner, + consts::PARAM_RECIPIENT => recipient, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn change_ownership(&mut self, owner: Address) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_CHANGE_OWNERSHIP, + runtime_args! { + consts::PARAM_OWNER => owner, + }, + ) + } + + fn add_to_whitelist(&mut self, address: Address) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_ADD_TO_WHITELIST, + runtime_args! { + consts::PARAM_ADDRESS => address, + }, + ) + } + + fn remove_from_whitelist(&mut self, address: Address) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_REMOVE_FROM_WHITELIST, + runtime_args! { + consts::PARAM_ADDRESS => address, + }, + ) + } + + fn stake(&mut self, address: Address, amount: U256) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_STAKE, + runtime_args! { + consts::PARAM_ADDRESS => address, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn unstake(&mut self, address: Address, amount: U256) { + runtime::call_versioned_contract( + self.contract_package_hash, + None, + consts::EP_UNSTAKE, + runtime_args! { + consts::PARAM_ADDRESS => address, + consts::PARAM_AMOUNT => amount + }, + ) + } +} + +#[cfg(feature = "test-support")] +mod tests { + use std::fmt::Debug; + + use casper_dao_utils::{consts, Address, TestEnv}; + use casper_types::bytesrepr::{Bytes, FromBytes}; + use casper_types::{runtime_args, ContractPackageHash, RuntimeArgs, U256}; + + use crate::{ReputationContract, ReputationContractInterface}; + + /// Implementation of the Reputation Contract Test. See [`ReputationContractInterface`]. + pub struct ReputationContractTest { + env: TestEnv, + package_hash: ContractPackageHash, + data: ReputationContract, + } + + impl ReputationContractTest { + pub fn new(env: &TestEnv) -> ReputationContractTest { + env.deploy_wasm_file("reputation_contract.wasm", runtime_args! {}); + let package_hash = env.get_contract_package_hash("reputation_contract_package_hash"); + ReputationContractTest { + env: env.clone(), + package_hash, + data: ReputationContract::default(), + } + } + + pub fn as_account(&mut self, account: Address) -> &mut Self { + self.env.as_account(account); + self + } + + pub fn get_owner(&self) -> Option
{ + self.env + .get_value(self.package_hash, self.data.owner.owner.path()) + } + + pub fn total_supply(&self) -> U256 { + self.env + .get_value(self.package_hash, self.data.token.token.total_supply.path()) + } + + pub fn balance_of(&self, address: Address) -> U256 { + self.env.get_dict_value( + self.package_hash, + self.data.token.token.balances.path(), + address, + ) + } + + pub fn is_whitelisted(&self, address: Address) -> bool { + self.env.get_dict_value( + self.package_hash, + self.data.whitelist.whitelist.path(), + address, + ) + } + + pub fn get_staked_balance_of(&self, address: Address) -> U256 { + self.env + .get_dict_value(self.package_hash, self.data.token.stakes.path(), address) + } + + pub fn event(&self, index: u32) -> T { + let raw_event: Bytes = self.env.get_dict_value(self.package_hash, "events", index); + let (event, bytes) = T::from_bytes(&raw_event).unwrap(); + assert!(bytes.is_empty()); + event + } + + pub fn assert_event_at(&self, index: u32, event: T) { + assert_eq!(self.event::(index), event); + } + } + + impl ReputationContractInterface for ReputationContractTest { + fn init(&mut self) { + self.env + .call_contract_package(self.package_hash, "init", runtime_args! {}) + } + + fn mint(&mut self, recipient: Address, amount: U256) { + self.env.call_contract_package( + self.package_hash, + consts::EP_MINT, + runtime_args! { + consts::PARAM_RECIPIENT => recipient, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn burn(&mut self, owner: Address, amount: U256) { + self.env.call_contract_package( + self.package_hash, + consts::EP_BURN, + runtime_args! { + consts::PARAM_OWNER => owner, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256) { + self.env.call_contract_package( + self.package_hash, + consts::EP_TRANSFER_FROM, + runtime_args! { + consts::PARAM_OWNER => owner, + consts::PARAM_RECIPIENT => recipient, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn change_ownership(&mut self, owner: Address) { + self.env.call_contract_package( + self.package_hash, + consts::EP_CHANGE_OWNERSHIP, + runtime_args! { + consts::PARAM_OWNER => owner + }, + ) + } + + fn add_to_whitelist(&mut self, address: Address) { + self.env.call_contract_package( + self.package_hash, + consts::EP_ADD_TO_WHITELIST, + runtime_args! { + consts::PARAM_ADDRESS => address + }, + ) + } + + fn remove_from_whitelist(&mut self, address: Address) { + self.env.call_contract_package( + self.package_hash, + consts::EP_REMOVE_FROM_WHITELIST, + runtime_args! { + consts::PARAM_ADDRESS => address + }, + ) + } + + fn stake(&mut self, address: Address, amount: U256) { + self.env.call_contract_package( + self.package_hash, + consts::EP_STAKE, + runtime_args! { + consts::PARAM_ADDRESS => address, + consts::PARAM_AMOUNT => amount + }, + ) + } + + fn unstake(&mut self, address: Address, amount: U256) { + self.env.call_contract_package( + self.package_hash, + consts::EP_UNSTAKE, + runtime_args! { + consts::PARAM_ADDRESS => address, + consts::PARAM_AMOUNT => amount + }, + ) + } + } +} + +#[cfg(feature = "test-support")] +pub use tests::ReputationContractTest; diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 00000000..fa84220f --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "casper-dao-macros" +version = "0.1.0" +edition = "2018" + +[dependencies] +proc-macro2 = "1.0.33" +quote = "1.0.15" +syn = "1.0.86" + +[lib] +proc-macro = true \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 00000000..af36f200 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,102 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, TokenStreamExt}; +use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields}; + +/// Derive events on top of any struct. +#[proc_macro_derive(Event)] +pub fn derive_events(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let struct_ident = input.ident.clone(); + let fields = match named_fields(input) { + Ok(fields) => fields, + Err(error_stream) => return error_stream, + }; + + let mut name_literal = TokenStream2::new(); + name_literal.append_all(quote! { + stringify!(#struct_ident) + }); + + let mut deserialize_fields = TokenStream2::new(); + deserialize_fields.append_all(fields.iter().map(|ident| { + quote! { + let (#ident, bytes) = casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; + } + })); + + let mut construct_struct = TokenStream2::new(); + construct_struct.append_all(fields.iter().map(|ident| quote! { #ident, })); + + let mut sum_serialized_lengths = TokenStream2::new(); + sum_serialized_lengths.append_all(quote! { + size += #name_literal.serialized_length(); + }); + sum_serialized_lengths.append_all(fields.iter().map(|ident| { + quote! { + size += self.#ident.serialized_length(); + } + })); + + let mut append_bytes = TokenStream2::new(); + append_bytes.append_all(fields.iter().map(|ident| { + quote! { + vec.extend(self.#ident.to_bytes()?); + } + })); + + let mut type_check = TokenStream2::new(); + type_check.append_all(quote! { + let (event_name, bytes): (String, _) = casper_types::bytesrepr::FromBytes::from_bytes(bytes)?; + if &event_name != #name_literal { + return core::result::Result::Err(casper_types::bytesrepr::Error::Formatting) + } + }); + + TokenStream::from(quote! { + impl casper_types::bytesrepr::FromBytes for #struct_ident { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), casper_types::bytesrepr::Error> { + #type_check + #deserialize_fields + let value = #struct_ident { + #construct_struct + }; + Ok((value, bytes)) + } + } + + impl casper_types::bytesrepr::ToBytes for #struct_ident { + fn serialized_length(&self) -> usize { + let mut size = 0; + #sum_serialized_lengths + return size; + } + + fn to_bytes(&self) -> Result, casper_types::bytesrepr::Error> { + let mut vec = Vec::with_capacity(self.serialized_length()); + vec.append(&mut #name_literal.to_bytes()?); + #append_bytes + Ok(vec) + } + } + }) +} + +fn named_fields(input: DeriveInput) -> Result, TokenStream> { + let fields = match input.data { + Data::Struct(DataStruct { + fields: Fields::Named(named_fields), + .. + }) => named_fields + .named + .into_iter() + .map(|x| x.ident.unwrap()) + .collect::>(), + _ => { + return Err(TokenStream::from( + quote! { compile_error!("Expected a struct with named fields."); }, + )) + } + }; + Ok(fields) +} diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 00000000..8398865b --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +nightly-2021-06-17 diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 00000000..b620ee9f --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "tests" +version = "0.1.0" +edition = "2018" + +[dev-dependencies] +casper-types = "1.4.6" +casper-dao-contracts = { path = "../contracts", default-features = false, features = [ "test-support" ] } +casper-dao-utils = { path = "../utils", features = [ "test-support" ]} + +[[bin]] +name = "reputation-tests" +path = "src/reputation_tests.rs" +bench = false +doctest = false +doc = false \ No newline at end of file diff --git a/tests/src/reputation_tests.rs b/tests/src/reputation_tests.rs new file mode 100644 index 00000000..76cb71e0 --- /dev/null +++ b/tests/src/reputation_tests.rs @@ -0,0 +1,368 @@ +#[cfg(test)] +mod tests { + use casper_dao_contracts::{ReputationContractInterface, ReputationContractTest}; + use casper_dao_utils::{ + owner::events::OwnerChanged, + staking::events::{TokensStaked, TokensUnstaked}, + token::events::{Burn, Mint, Transfer}, + whitelist::events::{AddedToWhitelist, RemovedFromWhitelist}, + Error, ExecutionError, TestEnv, + }; + use casper_types::U256; + + #[test] + fn test_deploy() { + let (env, contract) = setup(); + let deployer = env.get_account(0); + assert_eq!(contract.total_supply(), U256::zero()); + assert_eq!(contract.balance_of(env.get_account(0)), U256::zero()); + assert_eq!(contract.balance_of(env.get_account(1)), U256::zero()); + assert!(contract.is_whitelisted(contract.get_owner().unwrap())); + contract.assert_event_at( + 0, + OwnerChanged { + new_owner: deployer, + }, + ); + contract.assert_event_at(1, AddedToWhitelist { address: deployer }); + } + + #[test] + fn test_init_cannot_be_called_twice() { + let (env, mut contract) = setup(); + env.expect_execution_error(ExecutionError::InvalidContext); + contract.init(); + } + + #[test] + fn test_mint_as_owner() { + let (env, mut contract) = setup(); + let recipient = env.get_account(1); + let total_supply = 100.into(); + + contract.mint(recipient, total_supply); + assert_eq!(contract.balance_of(recipient), total_supply); + contract.assert_event_at( + 2, + Mint { + recipient, + value: total_supply, + }, + ); + } + + #[test] + fn test_mint_as_non_owner() { + let (env, mut contract) = setup(); + let non_owner = env.get_account(1); + + env.expect_error(Error::NotWhitelisted); + contract.as_account(non_owner).mint(non_owner, 10.into()); + } + + #[test] + fn test_whitelisted_user_burn() { + let total_supply = 100.into(); + let burn_amount = 10.into(); + let remaining_supply = total_supply - burn_amount; + + let (env, mut contract) = setup_with_initial_supply(total_supply); + let owner = env.get_account(0); + + contract.burn(owner, burn_amount); + assert_eq!(contract.total_supply(), remaining_supply); + assert_eq!(contract.balance_of(owner), remaining_supply); + contract.assert_event_at( + 3, + Burn { + owner, + value: burn_amount, + }, + ); + } + + #[test] + fn test_buring_amount_exceeding_balance() { + let total_supply = 100.into(); + let burn_amount = 101.into(); + + let (env, mut contract) = setup_with_initial_supply(total_supply); + let owner = env.get_account(0); + + env.expect_error(Error::InsufficientBalance); + contract.burn(owner, burn_amount); + } + + #[test] + fn test_non_whitelisted_user_burn() { + let (env, mut contract) = setup_with_initial_supply(100.into()); + let (user1, user2) = (env.get_account(0), env.get_account(1)); + + env.expect_error(Error::NotWhitelisted); + contract.as_account(user2).burn(user1, 10.into()); + } + + #[test] + fn test_total_supply_overflow() { + let (env, mut contract) = setup(); + + contract.mint(env.get_account(0), U256::MAX); + + env.expect_error(Error::TotalSupplyOverflow); + contract.mint(env.get_account(0), U256::one()); + } + + #[test] + fn test_whitelisting_as_owner() { + let (env, mut contract) = setup(); + let (owner, user) = (env.get_account(0), env.get_account(1)); + + assert!(contract.is_whitelisted(owner)); + assert_eq!(contract.is_whitelisted(user), false); + + contract.add_to_whitelist(user); + assert!(contract.is_whitelisted(user)); + contract.assert_event_at(2, AddedToWhitelist { address: user }); + + contract.remove_from_whitelist(user); + assert_eq!(contract.is_whitelisted(user), false); + contract.assert_event_at(3, RemovedFromWhitelist { address: user }); + } + + #[test] + fn test_not_whitelisted_user_removal_has_no_effect() { + let (env, mut contract) = setup(); + let user = env.get_account(1); + + assert_eq!(contract.is_whitelisted(user), false); + + contract.remove_from_whitelist(user); + assert_eq!(contract.is_whitelisted(user), false); + } + + #[test] + fn test_duplicated_whitelisting() { + let (env, mut contract) = setup(); + let user = env.get_account(1); + + contract.add_to_whitelist(user); + contract.add_to_whitelist(user); + assert!(contract.is_whitelisted(user)); + contract.assert_event_at(2, AddedToWhitelist { address: user }); + contract.assert_event_at(3, AddedToWhitelist { address: user }); + + contract.remove_from_whitelist(user); + assert_eq!(contract.is_whitelisted(user), false); + contract.assert_event_at(4, RemovedFromWhitelist { address: user }); + } + + #[test] + fn test_whitelisting_as_non_owner() { + let (env, mut contract) = setup(); + let (user1, user2) = (env.get_account(1), env.get_account(2)); + + contract.add_to_whitelist(user1); + + env.expect_error(Error::NotAnOwner); + contract.as_account(user1).add_to_whitelist(user2); + + env.expect_error(Error::NotAnOwner); + contract.as_account(user1).remove_from_whitelist(user2); + } + + #[test] + fn test_new_owner_whitelisting() { + let (env, mut contract) = setup(); + let (owner, new_owner) = (env.get_account(0), env.get_account(1)); + + assert!(contract.is_whitelisted(owner)); + + contract.change_ownership(new_owner); + assert!(contract.is_whitelisted(new_owner)); + contract.assert_event_at(2, OwnerChanged { new_owner }); + contract.assert_event_at(3, AddedToWhitelist { address: new_owner }); + } + + #[test] + fn test_transfer_from() { + let total_supply = 10.into(); + let transfer_amount = 4.into(); + + let (env, mut contract) = setup_with_initial_supply(total_supply); + let (owner, first_recipient) = (env.get_account(0), env.get_account(1)); + + contract.transfer_from(owner, first_recipient, transfer_amount); + + assert_eq!(contract.balance_of(owner), total_supply - transfer_amount); + assert_eq!(contract.balance_of(first_recipient), transfer_amount); + contract.assert_event_at( + 3, + Transfer { + from: owner, + to: first_recipient, + value: transfer_amount, + }, + ); + } + + #[test] + fn test_transfer_from_not_whitelisted_user() { + let (env, mut contract) = setup(); + let (sender, recipient) = (env.get_account(1), env.get_account(2)); + + contract.mint(sender, 10.into()); + + env.expect_error(Error::NotWhitelisted); + contract + .as_account(sender) + .transfer_from(sender, recipient, 1.into()); + } + + #[test] + fn test_transfer_amount_higher_than_balance() { + let total_supply = 10.into(); + let transfer_amount = 11.into(); + + let (env, mut contract) = setup_with_initial_supply(total_supply); + let (owner, first_recipient) = (env.get_account(0), env.get_account(1)); + + env.expect_error(Error::InsufficientBalance); + contract.transfer_from(owner, first_recipient, transfer_amount); + } + + #[test] + fn test_ownership() { + let (env, mut contract) = setup(); + let (owner, new_owner) = (env.get_account(0), env.get_account(1)); + assert_eq!(contract.get_owner().unwrap(), owner); + + contract.change_ownership(new_owner); + assert_eq!(contract.get_owner().unwrap(), new_owner); + + env.expect_error(Error::NotAnOwner); + contract.change_ownership(new_owner); + } + + #[test] + fn test_stake() { + let total_supply = 100.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let amount_to_stake = 10.into(); + let account = env.get_account(0); + + contract.stake(account, amount_to_stake); + assert_eq!(contract.balance_of(account), total_supply); + assert_eq!(contract.get_staked_balance_of(account), amount_to_stake); + contract.assert_event_at( + 3, + TokensStaked { + address: account, + amount: amount_to_stake, + }, + ); + } + + #[test] + fn test_stake_amount_exceeding_balance() { + let total_supply = 100.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let amount_to_stake = 200.into(); + let account = env.get_account(0); + + env.expect_error(Error::InsufficientBalance); + contract.stake(account, amount_to_stake); + } + + #[test] + fn test_stake_not_whitelisted() { + let (env, mut contract) = setup(); + let not_whitelisted_account = env.get_account(1); + + env.expect_error(Error::NotWhitelisted); + contract + .as_account(not_whitelisted_account) + .stake(not_whitelisted_account, 1.into()); + } + + #[test] + fn test_burn_staked_tokens() { + let total_supply = 100.into(); + let staked_amount = 10.into(); + let burn_amount = 99.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let owner = env.get_account(0); + + contract.stake(owner, staked_amount); + + env.expect_error(Error::InsufficientBalance); + contract.burn(owner, burn_amount); + } + + #[test] + fn test_transfer_staked_tokens() { + let total_supply = 100.into(); + let staked_amount = 10.into(); + let transferred_amount = 99.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let (owner, recipient) = (env.get_account(0), env.get_account(1)); + + contract.stake(owner, staked_amount); + + env.expect_error(Error::InsufficientBalance); + contract.transfer_from(owner, recipient, transferred_amount); + } + + #[test] + fn test_unstake() { + let total_supply = 100.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let amount_to_stake = 10.into(); + let amount_to_unstake = 4.into(); + let account = env.get_account(0); + + contract.stake(account, amount_to_stake); + contract.unstake(account, amount_to_unstake); + assert_eq!( + contract.get_staked_balance_of(account), + amount_to_stake - amount_to_unstake + ); + contract.assert_event_at( + 4, + TokensUnstaked { + address: account, + amount: amount_to_unstake, + }, + ); + } + + #[test] + fn test_unstake_amount_exceeding_staked_balance() { + let total_supply = 100.into(); + let (env, mut contract) = setup_with_initial_supply(total_supply); + let amount_to_stake = 50.into(); + let amount_to_unstake = 60.into(); + let account = env.get_account(0); + + contract.stake(account, amount_to_stake); + env.expect_error(Error::InsufficientBalance); + contract.unstake(account, amount_to_unstake); + } + + fn setup() -> (TestEnv, ReputationContractTest) { + let env = TestEnv::new(); + let contract = ReputationContractTest::new(&env); + + (env, contract) + } + + fn setup_with_initial_supply(amount: U256) -> (TestEnv, ReputationContractTest) { + let (env, mut contract) = setup(); + contract.mint(env.get_account(0), amount); + + (env, contract) + } +} + +fn main() { + panic!("Execute \"cargo test\" to test the contract, not \"cargo run\"."); +} diff --git a/tests/wasm/.gitkeep b/tests/wasm/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/utils/Cargo.toml b/utils/Cargo.toml new file mode 100644 index 00000000..da011dfb --- /dev/null +++ b/utils/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "casper-dao-utils" +version = "0.1.0" +edition = "2018" + +[dependencies] +casper-contract = { version = "1.4.3", default-features = false } +casper-types = "1.4.6" +base64 = "0.13.0" +lazy_static = "1.4.0" +casper-dao-macros = { path = "../macros" } + +casper-engine-test-support = { version = "2.0.3", features = ["test-support"], optional = true } +casper-execution-engine = { version = "1.4.4", optional = true } + +[features] +default = [ "casper-contract/default", "casper-contract/std" ] +test-support = [ "casper-engine-test-support", "casper-execution-engine" ] \ No newline at end of file diff --git a/utils/src/casper_env.rs b/utils/src/casper_env.rs new file mode 100644 index 00000000..dafed490 --- /dev/null +++ b/utils/src/casper_env.rs @@ -0,0 +1,92 @@ +//! Interact with the CasperVM inside the contract. + +use std::convert::TryInto; + +use casper_contract::{ + contract_api::{runtime, storage}, + unwrap_or_revert::UnwrapOrRevert, +}; +use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + system::CallStackElement, + CLTyped, +}; + +use crate::{Address, Events}; + +/// Read value from the storage. +pub fn get_key(name: &str) -> Option { + match runtime::get_key(name) { + None => None, + Some(value) => { + let key = value.try_into().unwrap_or_revert(); + let value = storage::read(key).unwrap_or_revert().unwrap_or_revert(); + Some(value) + } + } +} + +/// Save value to the storage. +pub fn set_key(name: &str, value: T) { + match runtime::get_key(name) { + Some(key) => { + let key_ref = key.try_into().unwrap_or_revert(); + storage::write(key_ref, value); + } + None => { + let key = storage::new_uref(value).into(); + runtime::put_key(name, key); + } + } +} + +/// Returns address based on a [`CallStackElement`]. +/// +/// For `Session` and `StoredSession` variants it will return account hash, and for `StoredContract` +/// case it will use contract hash as the address. +fn call_stack_element_to_address(call_stack_element: CallStackElement) -> Address { + match call_stack_element { + CallStackElement::Session { account_hash } => Address::from(account_hash), + CallStackElement::StoredSession { account_hash, .. } => { + // Stored session code acts in account's context, so if stored session + // wants to interact, caller's address will be used. + Address::from(account_hash) + } + CallStackElement::StoredContract { + contract_package_hash, + .. + } => Address::from(contract_package_hash), + } +} + +fn take_call_stack_elem(n: usize) -> CallStackElement { + runtime::get_call_stack() + .into_iter() + .nth_back(n) + .unwrap_or_revert() +} + +/// Gets the immediate session caller of the current execution. +/// +/// This function ensures that only session code can execute this function, and disallows stored +/// session/stored contracts. +pub fn caller() -> Address { + let second_elem = take_call_stack_elem(1); + call_stack_element_to_address(second_elem) +} + +/// Initialize events dictionary. +pub fn init_events() { + Events::default().init(); +} + +/// Record event to the contract's storage. +pub fn emit(event: T) { + Events::default().emit(event); +} + +/// Convert any key to base64. +pub fn to_dictionary_key(key: &T) -> String { + let preimage = key.to_bytes().unwrap_or_revert(); + base64::encode(&preimage) +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs new file mode 100644 index 00000000..926594ed --- /dev/null +++ b/utils/src/lib.rs @@ -0,0 +1,24 @@ +extern crate alloc; + +pub mod casper_env; +mod modules; +mod parts; +pub use casper_dao_macros; + +pub use parts::address::Address; +pub use parts::consts; +pub use parts::error::Error; +pub use parts::mapping::Mapping; +pub use parts::variable::Variable; + +pub use modules::owner; +pub use modules::staking; +pub use modules::token; +pub use modules::whitelist; +use modules::Events; + +#[cfg(feature = "test-support")] +mod test_env; + +#[cfg(feature = "test-support")] +pub use test_env::{ExecutionError, TestEnv}; diff --git a/utils/src/modules/events.rs b/utils/src/modules/events.rs new file mode 100644 index 00000000..c5fe12a9 --- /dev/null +++ b/utils/src/modules/events.rs @@ -0,0 +1,32 @@ +use casper_contract::unwrap_or_revert::UnwrapOrRevert; +use casper_types::bytesrepr::{Bytes, ToBytes}; + +use crate::{Mapping, Variable}; + +pub struct Events { + pub events: Mapping, + pub length: Variable, +} + +impl Default for Events { + fn default() -> Self { + Self { + events: Mapping::new(String::from("events")), + length: Variable::new(String::from("events_length")), + } + } +} + +impl Events { + pub fn init(&mut self) { + self.events.init(); + self.length.set(0); + } + + pub fn emit(&mut self, event: T) { + let lenght = self.length.get(); + let bytes: Bytes = event.to_bytes().unwrap_or_revert().into(); + self.events.set(&lenght, bytes); + self.length.set(lenght + 1); + } +} diff --git a/utils/src/modules/mod.rs b/utils/src/modules/mod.rs new file mode 100644 index 00000000..03bfe229 --- /dev/null +++ b/utils/src/modules/mod.rs @@ -0,0 +1,7 @@ +mod events; +pub mod owner; +pub mod staking; +pub mod token; +pub mod whitelist; + +pub(crate) use events::Events; diff --git a/utils/src/modules/owner.rs b/utils/src/modules/owner.rs new file mode 100644 index 00000000..ff8bf687 --- /dev/null +++ b/utils/src/modules/owner.rs @@ -0,0 +1,76 @@ +//! Single-owner-based access control system. + +use casper_contract::contract_api::runtime; + +use crate::{ + casper_env::{caller, emit}, + consts, Address, Error, Variable, +}; + +use self::events::OwnerChanged; + +/// The Owner module. +pub struct Owner { + pub owner: Variable>, +} + +impl Default for Owner { + fn default() -> Self { + Self { + owner: Variable::from(consts::NAME_OWNER), + } + } +} + +impl Owner { + /// Initialize the module. + pub fn init(&mut self, owner: Address) { + self.change_ownership(owner); + } + + /// Set the owner to the new address. + pub fn change_ownership(&mut self, owner: Address) { + self.owner.set(Some(owner)); + emit(OwnerChanged { new_owner: owner }); + } + + /// Verify if the contract caller is the owner. Revert otherwise. + pub fn ensure_owner(&self) { + if let Some(owner) = self.owner.get() { + if owner != caller() { + runtime::revert(Error::NotAnOwner) // User is not the owner. + } + } else { + runtime::revert(Error::OwnerIsNotInitialized) // Owner is not inicialized. + } + } +} + +pub mod entry_points { + //! Entry points definitions. + use crate::{consts, Address}; + use casper_types::{CLTyped, EntryPoint, EntryPointAccess, EntryPointType, Parameter}; + + /// Public `change_ownership` entry point. Corresponds to [`change_ownership`](super::Owner::change_ownership). + pub fn change_ownership() -> EntryPoint { + EntryPoint::new( + consts::EP_CHANGE_OWNERSHIP, + vec![Parameter::new(consts::PARAM_OWNER, Address::cl_type())], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } +} + +pub mod events { + //! Events definitions. + use crate::Address; + use casper_dao_macros::Event; + + /// Informs the owner change. + #[derive(Debug, PartialEq, Event)] + pub struct OwnerChanged { + pub new_owner: Address, + } +} diff --git a/utils/src/modules/staking.rs b/utils/src/modules/staking.rs new file mode 100644 index 00000000..78a1c253 --- /dev/null +++ b/utils/src/modules/staking.rs @@ -0,0 +1,131 @@ +//! Token with staking powers. + +use casper_contract::contract_api::runtime; +use casper_types::U256; + +use crate::{casper_env::emit, consts, token::Token, Address, Error, Mapping}; + +use self::events::{TokensStaked, TokensUnstaked}; + +/// The TokenWithStaking module. +pub struct TokenWithStaking { + pub stakes: Mapping, + pub token: Token, +} + +impl Default for TokenWithStaking { + fn default() -> Self { + Self { + stakes: Mapping::from(consts::NAME_STAKES), + token: Token::default(), + } + } +} + +impl TokenWithStaking { + /// Initialize the module. + pub fn init(&mut self) { + self.stakes.init(); + self.token.init(); + } + + /// Mint new tokens. See [`Token::mint`](Token::mint). + pub fn mint(&mut self, recipient: Address, amount: U256) { + self.token.mint(recipient, amount); + } + + /// Burn unstaked tokens. See [`Token::burn`](Token::burn) + pub fn burn(&mut self, owner: Address, amount: U256) { + self.ensure_balance(&owner, amount); + self.token.burn(owner, amount); + } + + /// Transfer unstaked tokens. See [`Token::raw_transfer`](Token::raw_transfer) + pub fn raw_transfer(&mut self, sender: Address, recipient: Address, amount: U256) { + self.ensure_balance(&sender, amount); + self.token.raw_transfer(sender, recipient, amount); + } + + /// Stake `amount` of tokens for the `address`. It decrements `address`'s balance by `amount`. + pub fn stake(&mut self, address: Address, amount: U256) { + self.ensure_balance(&address, amount); + self.stakes + .set(&address, self.stakes.get(&address) + amount); + emit(TokensStaked { address, amount }); + } + + /// Unstake `amount` of tokens for the `address`. It increments `address`'s balance by `amount`. + pub fn unstake(&mut self, address: Address, amount: U256) { + self.ensure_staked_balance(&address, amount); + self.stakes + .set(&address, self.stakes.get(&address) - amount); + emit(TokensUnstaked { address, amount }); + } + + fn ensure_balance(&mut self, address: &Address, amount: U256) { + let staked_amount = self.stakes.get(address); + self.token.ensure_balance(address, staked_amount + amount); + } + + fn ensure_staked_balance(&mut self, address: &Address, amount: U256) { + if self.stakes.get(address) < amount { + runtime::revert(Error::InsufficientBalance); + } + } +} + +pub mod entry_points { + //! Entry points definitions. + pub use crate::token::entry_points::{burn, mint, transfer_from}; + use crate::{consts, Address}; + use casper_types::{CLTyped, EntryPoint, EntryPointAccess, EntryPointType, Parameter, U256}; + + /// Public `stake` entry point. Corresponds to [`stake`](super::TokenWithStaking::stake). + pub fn stake() -> EntryPoint { + EntryPoint::new( + consts::EP_STAKE, + vec![ + Parameter::new(consts::PARAM_OWNER, Address::cl_type()), + Parameter::new(consts::PARAM_AMOUNT, U256::cl_type()), + ], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } + + /// Public `unstake` entry point. Corresponds to [`unstake`](super::TokenWithStaking::unstake). + pub fn unstake() -> EntryPoint { + EntryPoint::new( + consts::EP_UNSTAKE, + vec![ + Parameter::new(consts::PARAM_OWNER, Address::cl_type()), + Parameter::new(consts::PARAM_AMOUNT, U256::cl_type()), + ], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } +} + +pub mod events { + //! Events definitions. + use crate::Address; + use casper_dao_macros::Event; + use casper_types::U256; + + /// Informs tokens have been staked. + #[derive(Debug, PartialEq, Event)] + pub struct TokensStaked { + pub address: Address, + pub amount: U256, + } + + /// Informs tokens have been unstaked. + #[derive(Debug, PartialEq, Event)] + pub struct TokensUnstaked { + pub address: Address, + pub amount: U256, + } +} diff --git a/utils/src/modules/token.rs b/utils/src/modules/token.rs new file mode 100644 index 00000000..c9529c22 --- /dev/null +++ b/utils/src/modules/token.rs @@ -0,0 +1,168 @@ +//! Token module with balances and total supply. + +use casper_contract::contract_api::runtime; +use casper_types::U256; + +use self::events::{Burn, Mint, Transfer}; +use crate::{casper_env::emit, consts, Address, Error, Mapping, Variable}; + +/// The Token module. +pub struct Token { + pub total_supply: Variable, + pub balances: Mapping, +} + +impl Default for Token { + fn default() -> Self { + Self { + total_supply: Variable::from(consts::NAME_TOTAL_SUPPLY), + balances: Mapping::from(consts::NAME_BALANCES), + } + } +} + +impl Token { + /// Initialize the module. + pub fn init(&mut self) { + self.balances.init(); + self.total_supply.set(U256::zero()); + } + + /// Mint new tokens. + /// + /// Add `amount` of new tokens to the balance of the `recipient` and + /// increment the total supply. + /// + /// It emits [`Mint`](events::Mint) event. + pub fn mint(&mut self, recipient: Address, amount: U256) { + let (new_supply, is_overflowed) = self.total_supply.get().overflowing_add(amount); + if is_overflowed { + runtime::revert(Error::TotalSupplyOverflow); + } + + self.total_supply.set(new_supply); + self.balances + .set(&recipient, self.balances.get(&recipient) + amount); + + emit(Mint { + recipient, + value: amount, + }); + } + + /// Burn existing tokens. + /// + /// Remove `amount` of existing tokens from the balance of the `owner` + /// and decrement the total supply. + /// + /// It emits [`Burn`](events::Burn) event. + pub fn burn(&mut self, owner: Address, amount: U256) { + self.total_supply.set(self.total_supply.get() - amount); + self.balances + .set(&owner, self.balances.get(&owner) - amount); + emit(Burn { + owner, + value: amount, + }); + } + + /// Transfer `amount` of tokens from `owner` to `recipient`. + /// + /// It emits [`Transfer`](events::Transfer) event. + pub fn raw_transfer(&mut self, sender: Address, recipient: Address, amount: U256) { + self.balances + .set(&sender, self.balances.get(&sender) - amount); + self.balances + .set(&recipient, self.balances.get(&recipient) + amount); + + emit(Transfer { + from: sender, + to: recipient, + value: amount, + }); + } + + /// Assert `address` has at least `amount` of tokens. + /// + /// Revert otherwise. + pub fn ensure_balance(&mut self, address: &Address, amount: U256) { + if self.balances.get(address) < amount { + runtime::revert(Error::InsufficientBalance); + } + } +} + +pub mod entry_points { + //! Entry points definitions. + use crate::{consts, Address}; + use casper_types::{CLTyped, EntryPoint, EntryPointAccess, EntryPointType, Parameter, U256}; + + /// Public `mint` entry point. Corresponds to [`mint`](super::Token::mint). + pub fn mint() -> EntryPoint { + EntryPoint::new( + consts::EP_MINT, + vec![ + Parameter::new(consts::PARAM_RECIPIENT, Address::cl_type()), + Parameter::new(consts::PARAM_AMOUNT, U256::cl_type()), + ], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } + + /// Public `burn` entry point. Corresponds to [`burn`](super::Token::burn). + pub fn burn() -> EntryPoint { + EntryPoint::new( + consts::EP_BURN, + vec![ + Parameter::new(consts::PARAM_OWNER, Address::cl_type()), + Parameter::new(consts::PARAM_AMOUNT, U256::cl_type()), + ], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } + + /// Public `transfer_from` entry point. Corresponds to [`raw_transfer`](super::Token::raw_transfer). + pub fn transfer_from() -> EntryPoint { + EntryPoint::new( + consts::EP_TRANSFER_FROM, + vec![ + Parameter::new(consts::PARAM_OWNER, Address::cl_type()), + Parameter::new(consts::PARAM_RECIPIENT, Address::cl_type()), + Parameter::new(consts::PARAM_AMOUNT, U256::cl_type()), + ], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } +} + +pub mod events { + use casper_dao_macros::Event; + use casper_types::U256; + + use crate::Address; + + #[derive(Debug, PartialEq, Event)] + pub struct Transfer { + pub from: Address, + pub to: Address, + pub value: U256, + } + + #[derive(Debug, PartialEq, Event)] + pub struct Mint { + pub recipient: Address, + pub value: U256, + } + + #[derive(Debug, PartialEq, Event)] + pub struct Burn { + pub owner: Address, + pub value: U256, + } +} diff --git a/utils/src/modules/whitelist.rs b/utils/src/modules/whitelist.rs new file mode 100644 index 00000000..f500341a --- /dev/null +++ b/utils/src/modules/whitelist.rs @@ -0,0 +1,96 @@ +//! Whitelist-based access control system. + +use casper_contract::contract_api::runtime; + +use crate::{ + casper_env::{caller, emit}, + consts, Address, Error, Mapping, +}; + +use self::events::{AddedToWhitelist, RemovedFromWhitelist}; + +/// The Whitelist module. +pub struct Whitelist { + pub whitelist: Mapping, +} + +impl Default for Whitelist { + fn default() -> Self { + Self { + whitelist: Mapping::from(consts::NAME_WHITELIST), + } + } +} + +impl Whitelist { + /// Initialize the module. + pub fn init(&mut self) { + self.whitelist.init(); + } + + /// Add new `address` to the whitelist. + pub fn add_to_whitelist(&mut self, address: Address) { + self.whitelist.set(&address, true); + emit(AddedToWhitelist { address }); + } + + /// Remove an `address` from the whitelist. + pub fn remove_from_whitelist(&mut self, address: Address) { + self.whitelist.set(&address, false); + emit(RemovedFromWhitelist { address }); + } + + /// Assert the caller is on the list. Revert otherwise. + pub fn ensure_whitelisted(&self) { + if !self.whitelist.get(&caller()) { + runtime::revert(Error::NotWhitelisted); + } + } +} + +pub mod entry_points { + //! Entry points definitions. + + use crate::{consts, Address}; + use casper_types::{CLTyped, EntryPoint, EntryPointAccess, EntryPointType, Parameter}; + + /// Public `add_to_whitelist` entry point. Corresponds to [`add_to_whitelist`](super::Whitelist::add_to_whitelist). + pub fn add_to_whitelist() -> EntryPoint { + EntryPoint::new( + consts::EP_ADD_TO_WHITELIST, + vec![Parameter::new(consts::PARAM_ADDRESS, Address::cl_type())], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } + + /// Public `remove_from_whitelist` entry point. Corresponds to [`remove_from_whitelist`](super::Whitelist::remove_from_whitelist). + pub fn remove_from_whitelist() -> EntryPoint { + EntryPoint::new( + consts::EP_REMOVE_FROM_WHITELIST, + vec![Parameter::new(consts::PARAM_ADDRESS, Address::cl_type())], + <()>::cl_type(), + EntryPointAccess::Public, + EntryPointType::Contract, + ) + } +} + +pub mod events { + //! Events definitions. + use crate::Address; + use casper_dao_macros::Event; + + /// Informs new address has been added to the whitelist. + #[derive(Debug, PartialEq, Event)] + pub struct AddedToWhitelist { + pub address: Address, + } + + /// Informs new address has been removed from the whitelist. + #[derive(Debug, PartialEq, Event)] + pub struct RemovedFromWhitelist { + pub address: Address, + } +} diff --git a/utils/src/parts/address.rs b/utils/src/parts/address.rs new file mode 100644 index 00000000..5541b2ea --- /dev/null +++ b/utils/src/parts/address.rs @@ -0,0 +1,92 @@ +use alloc::vec::Vec; +use casper_types::{ + account::AccountHash, + bytesrepr::{self, FromBytes, ToBytes}, + CLType, CLTyped, ContractPackageHash, Key, +}; + +/// An enum representing an [`AccountHash`] or a [`ContractPackageHash`]. +/// +/// It is taken from [`CasperLabs's ERC20`](https://raw.githubusercontent.com/casper-ecosystem/erc20/master/erc20/src/address.rs). +/// It is copied instead of imported for the flexebility. +#[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone, Copy, Debug)] +pub enum Address { + /// Represents an account hash. + Account(AccountHash), + /// Represents a contract package hash. + Contract(ContractPackageHash), +} + +impl Address { + /// Returns the inner account hash if `self` is the `Account` variant. + pub fn as_account_hash(&self) -> Option<&AccountHash> { + if let Self::Account(v) = self { + Some(v) + } else { + None + } + } + + /// Returns the inner contract hash if `self` is the `Contract` variant. + pub fn as_contract_package_hash(&self) -> Option<&ContractPackageHash> { + if let Self::Contract(v) = self { + Some(v) + } else { + None + } + } +} + +impl From for Address { + fn from(contract_package_hash: ContractPackageHash) -> Self { + Self::Contract(contract_package_hash) + } +} + +impl From for Address { + fn from(account_hash: AccountHash) -> Self { + Self::Account(account_hash) + } +} + +impl From
for Key { + fn from(address: Address) -> Self { + match address { + Address::Account(account_hash) => Key::Account(account_hash), + Address::Contract(contract_package_hash) => Key::Hash(contract_package_hash.value()), + } + } +} + +impl CLTyped for Address { + fn cl_type() -> casper_types::CLType { + CLType::Key + } +} + +impl ToBytes for Address { + fn to_bytes(&self) -> Result, bytesrepr::Error> { + Key::from(*self).to_bytes() + } + + fn serialized_length(&self) -> usize { + Key::from(*self).serialized_length() + } +} + +impl FromBytes for Address { + fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), bytesrepr::Error> { + let (key, remainder) = Key::from_bytes(bytes)?; + + let address = match key { + Key::Account(account_hash) => Address::Account(account_hash), + Key::Hash(raw_contract_package_hash) => { + let contract_package_hash = ContractPackageHash::new(raw_contract_package_hash); + Address::Contract(contract_package_hash) + } + _ => return Err(bytesrepr::Error::Formatting), + }; + + Ok((address, remainder)) + } +} diff --git a/utils/src/parts/consts.rs b/utils/src/parts/consts.rs new file mode 100644 index 00000000..218bfe8b --- /dev/null +++ b/utils/src/parts/consts.rs @@ -0,0 +1,22 @@ +//! Useful constants for common strings. + +pub const EP_INIT: &str = "init"; +pub const EP_MINT: &str = "mint"; +pub const EP_BURN: &str = "burn"; +pub const EP_TRANSFER_FROM: &str = "transfer_from"; +pub const EP_STAKE: &str = "stake"; +pub const EP_UNSTAKE: &str = "unstake"; +pub const EP_REMOVE_FROM_WHITELIST: &str = "remove_from_whitelist"; +pub const EP_ADD_TO_WHITELIST: &str = "add_to_whitelist"; +pub const EP_CHANGE_OWNERSHIP: &str = "change_ownership"; + +pub const PARAM_RECIPIENT: &str = "recipient"; +pub const PARAM_AMOUNT: &str = "amount"; +pub const PARAM_OWNER: &str = "owner"; +pub const PARAM_ADDRESS: &str = "address"; + +pub const NAME_OWNER: &str = "owner"; +pub const NAME_STAKES: &str = "stakes"; +pub const NAME_TOTAL_SUPPLY: &str = "total_supply"; +pub const NAME_BALANCES: &str = "balances"; +pub const NAME_WHITELIST: &str = "whitelist"; diff --git a/utils/src/parts/error.rs b/utils/src/parts/error.rs new file mode 100644 index 00000000..7ad4ae95 --- /dev/null +++ b/utils/src/parts/error.rs @@ -0,0 +1,23 @@ +use casper_types::ApiError; + +/// All possible errors that can be raised by the utils crate. +pub enum Error { + NotAnOwner, + OwnerIsNotInitialized, + NotWhitelisted, + InsufficientBalance, + TotalSupplyOverflow, +} + +impl From for ApiError { + fn from(val: Error) -> Self { + let id = match val { + Error::NotAnOwner => 1000, + Error::OwnerIsNotInitialized => 1001, + Error::NotWhitelisted => 1002, + Error::InsufficientBalance => 1003, + Error::TotalSupplyOverflow => 1004, + }; + ApiError::User(id) + } +} diff --git a/utils/src/parts/mapping.rs b/utils/src/parts/mapping.rs new file mode 100644 index 00000000..80301a5a --- /dev/null +++ b/utils/src/parts/mapping.rs @@ -0,0 +1,82 @@ +use std::{collections::BTreeMap, marker::PhantomData, sync::Mutex}; + +use casper_contract::{ + contract_api::{runtime, storage}, + unwrap_or_revert::UnwrapOrRevert, +}; +use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + CLTyped, Key, URef, +}; +use lazy_static::lazy_static; + +use crate::casper_env::to_dictionary_key; + +/// Data structure for storing key-value pairs. +/// +/// It's is a wrapper on top of Casper's dictionary. +/// The main difference is that Mapping returns default value, if the value doesn't exists +/// and it stores dictionary's URef for later use. +pub struct Mapping { + name: String, + key_ty: PhantomData, + value_ty: PhantomData, +} + +lazy_static! { + static ref SEEDS: Mutex> = Mutex::new(BTreeMap::new()); +} + +impl Mapping { + /// Create new Mapping instance. + pub fn new(name: String) -> Self { + Mapping { + name, + key_ty: PhantomData::::default(), + value_ty: PhantomData::::default(), + } + } + + /// Create dictionary's URef. + pub fn init(&self) { + storage::new_dictionary(&self.name).unwrap_or_revert(); + } + + /// Read `key` from the storage or return default value. + pub fn get(&self, key: &K) -> V { + storage::dictionary_get(self.get_uref(), &to_dictionary_key(key)) + .unwrap_or_revert() + .unwrap_or_default() + } + + /// Set `value` under `key` to the storage. It overrides by default. + pub fn set(&self, key: &K, value: V) { + storage::dictionary_put(self.get_uref(), &to_dictionary_key(key), value); + } + + /// Return the named key path to the dictionarie's URef. + pub fn path(&self) -> &str { + &self.name + } + + fn get_uref(&self) -> URef { + let mut seeds = SEEDS.lock().unwrap(); + match seeds.get(&self.name) { + Some(seed) => *seed, + None => { + let key: Key = runtime::get_key(&self.name).unwrap_or_revert(); + let seed: URef = *key.as_uref().unwrap_or_revert(); + seeds.insert(self.name.clone(), seed); + seed + } + } + } +} + +impl From<&str> + for Mapping +{ + fn from(name: &str) -> Self { + Mapping::new(name.to_string()) + } +} diff --git a/utils/src/parts/mod.rs b/utils/src/parts/mod.rs new file mode 100644 index 00000000..dc960c39 --- /dev/null +++ b/utils/src/parts/mod.rs @@ -0,0 +1,5 @@ +pub mod address; +pub mod consts; +pub mod error; +pub mod mapping; +pub mod variable; diff --git a/utils/src/parts/variable.rs b/utils/src/parts/variable.rs new file mode 100644 index 00000000..1c9db75d --- /dev/null +++ b/utils/src/parts/variable.rs @@ -0,0 +1,45 @@ +use std::marker::PhantomData; + +use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + CLTyped, +}; + +use crate::casper_env::{get_key, set_key}; + +/// Data structure for storing a single value. +pub struct Variable { + name: String, + ty: PhantomData, +} + +impl Variable { + /// Create a new Variable instance. + pub fn new(name: String) -> Self { + Variable { + name, + ty: PhantomData::::default(), + } + } + + /// Read from the storage or return default value. + pub fn get(&self) -> T { + get_key(&self.name).unwrap_or_default() + } + + /// Store `value` to the storage. + pub fn set(&mut self, value: T) { + set_key(&self.name, value); + } + + /// Return the named key path to the variable's URef. + pub fn path(&self) -> &str { + &self.name + } +} + +impl From<&str> for Variable { + fn from(name: &str) -> Self { + Variable::new(name.to_string()) + } +} diff --git a/utils/src/test_env.rs b/utils/src/test_env.rs new file mode 100644 index 00000000..52d34815 --- /dev/null +++ b/utils/src/test_env.rs @@ -0,0 +1,293 @@ +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use crate::Address; +use casper_engine_test_support::{ + DeployItemBuilder, ExecuteRequestBuilder, InMemoryWasmTestBuilder, ARG_AMOUNT, + DEFAULT_ACCOUNT_INITIAL_BALANCE, DEFAULT_GENESIS_CONFIG, DEFAULT_GENESIS_CONFIG_HASH, + DEFAULT_PAYMENT, +}; +use casper_execution_engine::core::{ + engine_state::{self, run_genesis_request::RunGenesisRequest, GenesisAccount}, + execution, +}; +use casper_types::{ + account::AccountHash, + bytesrepr::{FromBytes, ToBytes}, + runtime_args, ApiError, CLTyped, ContractPackageHash, Key, Motes, PublicKey, RuntimeArgs, + SecretKey, URef, U512, +}; + +pub use casper_execution_engine::core::execution::Error as ExecutionError; + +/// CasperVM based testing environment. +#[derive(Clone)] +pub struct TestEnv { + state: Arc>, +} + +impl TestEnv { + /// Create new TestEnv. + pub fn new() -> TestEnv { + TestEnv { + state: Arc::new(Mutex::new(TestEnvState::new())), + } + } + + /// Deploy new wasm file. + pub fn deploy_wasm_file(&self, session_code: &str, session_args: RuntimeArgs) { + self.state + .lock() + .unwrap() + .deploy_wasm_file(session_code, session_args); + } + + /// Call already deployed contract. + pub fn call_contract_package( + &mut self, + hash: ContractPackageHash, + entry_point: &str, + args: RuntimeArgs, + ) { + self.state + .lock() + .unwrap() + .call_contract_package(hash, entry_point, args); + } + + /// Read [`ContractPackageHash`] from the active user's named keys. + pub fn get_contract_package_hash(&self, name: &str) -> ContractPackageHash { + self.state.lock().unwrap().get_contract_package_hash(name) + } + + /// Read [`casper_types::CLValue`] from the contract's named keys. + pub fn get_value(&self, hash: ContractPackageHash, name: &str) -> T { + self.state.lock().unwrap().get_value(hash, name) + } + + /// Read [`casper_types::CLValue`] from the contract's dictionary. + pub fn get_dict_value( + &self, + hash: ContractPackageHash, + name: &str, + key: K, + ) -> V { + self.state.lock().unwrap().get_dict_value(hash, name, key) + } + + /// Get account by index. + pub fn get_account(&self, n: usize) -> Address { + self.state.lock().unwrap().get_account(n) + } + + /// Set the account context. + pub fn as_account(&self, account: Address) { + self.state.lock().unwrap().as_account(account); + } + + /// Set the [`ApiError`] expected to occur. + pub fn expect_error>(&self, error: T) { + self.state.lock().unwrap().expect_error(error); + } + + /// Set the [`execution::Error`] expected to occur. + pub fn expect_execution_error(&self, error: execution::Error) { + self.state.lock().unwrap().expect_execution_error(error); + } +} + +impl Default for TestEnv { + fn default() -> Self { + TestEnv::new() + } +} + +pub struct TestEnvState { + accounts: Vec
, + active_account: Address, + context: InMemoryWasmTestBuilder, + expected_error: Option, +} + +impl TestEnvState { + pub fn new() -> TestEnvState { + let mut genesis_config = DEFAULT_GENESIS_CONFIG.clone(); + let mut accounts: Vec
= Vec::new(); + for i in 0..3 { + // Create keypair. + let secret_key = SecretKey::ed25519_from_bytes([i; 32]).unwrap(); + let public_key = PublicKey::from(&secret_key); + + // Create an AccountHash from a public key. + let account_addr = AccountHash::from(&public_key); + + // Create a GenesisAccount. + let account = GenesisAccount::account( + public_key, + Motes::new(U512::from(DEFAULT_ACCOUNT_INITIAL_BALANCE)), + None, + ); + genesis_config.ee_config_mut().push_account(account); + + accounts.push(account_addr.into()); + } + + let run_genesis_request = RunGenesisRequest::new( + *DEFAULT_GENESIS_CONFIG_HASH, + genesis_config.protocol_version(), + genesis_config.take_ee_config(), + ); + + let mut builder = InMemoryWasmTestBuilder::default(); + builder.run_genesis(&run_genesis_request).commit(); + + TestEnvState { + active_account: accounts[0], + context: builder, + accounts, + expected_error: None, + } + } + + pub fn deploy_wasm_file(&mut self, wasm_path: &str, args: RuntimeArgs) { + let session_code = PathBuf::from(wasm_path); + + let deploy_item = DeployItemBuilder::new() + .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT}) + .with_authorization_keys(&[self.active_account_hash()]) + .with_address(self.active_account_hash()) + .with_session_code(session_code, args) + .build(); + + let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item).build(); + self.context.exec(execute_request).commit().expect_success(); + } + + pub fn call_contract_package( + &mut self, + hash: ContractPackageHash, + entry_point: &str, + args: RuntimeArgs, + ) { + let deploy_item = DeployItemBuilder::new() + .with_empty_payment_bytes(runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT}) + .with_authorization_keys(&[self.active_account_hash()]) + .with_address(self.active_account_hash()) + .with_stored_versioned_contract_by_hash(hash.value(), None, entry_point, args) + .build(); + + let execute_request = ExecuteRequestBuilder::from_deploy_item(deploy_item).build(); + self.context.exec(execute_request).commit(); + + if let Some(expected_error) = self.expected_error.clone() { + // If error is expected. + if self.context.is_error() { + // The execution actually ended with an error. + if let engine_state::Error::Exec(exec_error) = self.context.get_error().unwrap() { + assert_eq!(expected_error.to_string(), exec_error.to_string()); + self.expected_error = None; + } else { + panic!("Unexpected engine_state error."); + } + } else { + panic!("Deploy expected to fail."); + } + } else { + // If error is not expected. + if self.context.is_error() { + self.context.expect_success(); + } + } + self.active_account = self.get_account(0); + } + + pub fn get_contract_package_hash(&self, name: &str) -> ContractPackageHash { + let account = self + .context + .get_account(self.active_account_hash()) + .unwrap(); + let key: &Key = account.named_keys().get(name).unwrap(); + ContractPackageHash::from(key.into_hash().unwrap()) + } + + pub fn get_value(&self, hash: ContractPackageHash, name: &str) -> T { + let contract_hash = self + .context + .get_contract_package(hash) + .unwrap() + .current_contract_hash() + .unwrap(); + + self.context + .query(None, Key::Hash(contract_hash.value()), &[name.to_string()]) + .unwrap() + .as_cl_value() + .unwrap() + .clone() + .into_t() + .unwrap() + } + + pub fn get_dict_value( + &self, + hash: ContractPackageHash, + name: &str, + key: K, + ) -> V { + let contract_hash = self + .context + .get_contract_package(hash) + .unwrap() + .current_contract_hash() + .unwrap(); + + let dictionary_seed_uref: URef = *self + .context + .get_contract(contract_hash) + .unwrap() + .named_keys() + .get(name) + .unwrap() + .as_uref() + .unwrap(); + + match self.context.query_dictionary_item( + None, + dictionary_seed_uref, + &to_dictionary_key(&key), + ) { + Ok(val) => { + let value: V = val.as_cl_value().unwrap().clone().into_t::().unwrap(); + value + } + Err(_) => V::default(), + } + } + + fn active_account_hash(&self) -> AccountHash { + *self.active_account.as_account_hash().unwrap() + } + + pub fn get_account(&self, n: usize) -> Address { + *self.accounts.get(n).unwrap() + } + + pub fn as_account(&mut self, account: Address) { + self.active_account = account; + } + + pub fn expect_error>(&mut self, error: T) { + self.expect_execution_error(execution::Error::Revert(error.into())); + } + + pub fn expect_execution_error(&mut self, error: execution::Error) { + self.expected_error = Some(error); + } +} + +fn to_dictionary_key(key: &T) -> String { + let preimage = key.to_bytes().unwrap(); + base64::encode(&preimage) +}