diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..e89e247 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,45 @@ +name: build-test + +on: + pull_request: + paths-ignore: + - "*.md" + push: + paths-ignore: + - "*.md" + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + sudo apt update && sudo apt install -y libssl-dev pkg-config jq && \ + cargo install cargo-make && \ + wget https://download.dfinity.systems/ic/69e1408347723dbaa7a6cd2faa9b65c42abbe861/openssl-static-binaries/x86_64-linux/pocket-ic.gz && \ + gzip -d pocket-ic.gz && \ + chmod +x pocket-ic && \ + mv pocket-ic ./integration-tests/pocket-ic + - name: Install dfx + run: echo y | sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)" + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: rustfmt, clippy + target: wasm32-unknown-unknown + - name: Unit Tests + run: cargo make test + - name: Deploy local + run: cargo make deploy-local + - name: Integration Tests + run: cargo make integration-tests + - name: Format + run: cargo make check-format + - name: Lint + run: cargo make lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90394b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +# Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.env + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temp files +!.dfx/local/canisters/xrc/xrc.wasm +.dfx/ + +integration-tests/pocket-ic + diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..97fbfef --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1564 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "binread" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16598dfc8e6578e9b597d9910ba2e73618385dc9f4b1d43dd92c349d6be6418f" +dependencies = [ + "binread_derive", + "lazy_static", + "rustversion", +] + +[[package]] +name = "binread_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9672209df1714ee804b1f4d4f68c8eb2a90b1f7a07acf472f88ce198ef1fed" +dependencies = [ + "either", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +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 = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "candid" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465c1ce01d8089ee5b49ba20d3a9da15a28bba64c35cdff2aa256d37e319625d" +dependencies = [ + "anyhow", + "binread", + "byteorder", + "candid_derive", + "codespan-reporting", + "convert_case", + "crc32fast", + "data-encoding", + "hex", + "lalrpop", + "lalrpop-util", + "leb128", + "logos", + "num-bigint", + "num-traits", + "num_enum", + "paste", + "pretty", + "serde", + "serde_bytes", + "sha2", + "stacker", + "thiserror", +] + +[[package]] +name = "candid_derive" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201ea498d901add0822653ac94cb0f8a92f9b1758a5273f4dafbb6673c9a5020" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "ena" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1" +dependencies = [ + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ic-cdk" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c126ac20219abff15c3441282e9da6aa7244319d5a4a42c7260667237e790712" +dependencies = [ + "candid", + "ic-cdk-macros", + "ic0", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6295fd7389c198a97dd99b28b846e18487d99303077102d817eebbf6a924cd" +dependencies = [ + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 1.0.109", +] + +[[package]] +name = "ic-cdk-timers" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fa62243d3a412ceae88d6ad213614769e8b0bbcaeb98abb2e7985074257356" +dependencies = [ + "futures", + "ic-cdk", + "ic0", + "serde", + "serde_bytes", + "slotmap", +] + +[[package]] +name = "ic-stable-structures" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1edc2c31d16b3065e5162cc019bdcec380b07294bcf865533f072ea92268621" +dependencies = [ + "ic_principal", +] + +[[package]] +name = "ic0" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54b5297861c651551676e8c43df805dad175cc33bc97dbd992edbbb85dcbcdf" + +[[package]] +name = "ic_principal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" + +[[package]] +name = "icrc-ledger-types" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafdc6d688022822cda32f534ea13bb0b72b5f1a82235fbb4bc24250b1d8418b" +dependencies = [ + "base32", + "candid", + "crc32fast", + "hex", + "num-bigint", + "num-traits", + "serde", + "serde_bytes", + "sha2", +] + +[[package]] +name = "icrc2_template_canister" +version = "0.1.0" +dependencies = [ + "bytes", + "candid", + "ic-cdk", + "ic-cdk-macros", + "ic-cdk-timers", + "ic-stable-structures", + "icrc-ledger-types", + "num-bigint", + "num-traits", + "pretty_assertions", + "rand", + "serde", + "thiserror", + "tokio", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "lalrpop" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax 0.7.5", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "logos" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c000ca4d908ff18ac99b93a062cb8958d331c3220719c52e77cb19cc6ac5d2c1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-codegen" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc487311295e0002e452025d6b580b77bb17286de87b57138f3b5db711cded68" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 2.0.48", +] + +[[package]] +name = "logos-derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfc0d229f1f42d790440136d941afd806bc9e949e2bcb8faa813b0f00d1267e" +dependencies = [ + "logos-codegen", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "pretty" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_tokenstream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797ba1d80299b264f3aac68ab5d12e5825a561749db4df7cd7c8083900c5d4e9" +dependencies = [ + "proc-macro2", + "serde", + "syn 1.0.109", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "winapi", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e5175c4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "icrc2_template_canister" +authors = ["Christian Visintin "] +edition = "2021" +license = "MIT" +readme = "README.md" +description = "ICRC2 Template Canister" +repository = "https://github.com/veeso-dev/icrc2-template-canister" +version = "0.1.0" + +[[bin]] +name = "icrc2-template-canister-did" +path = "src/lib.rs" + +[lib] +name = "icrc2_template_canister" +crate-type = ["cdylib"] + +[features] +default = [] +did = [] + +[dependencies] +bytes = "1.5" +candid = "0.9" +ic-cdk = "0.11" +ic-cdk-macros = "0.8" +ic-cdk-timers = "0.5" +ic-stable-structures = "0.6" +icrc-ledger-types = "0.1" +num-bigint = "0.4" +num-traits = "0.2" +serde = { version = "1", features = ["derive"] } +thiserror = "1.0" + +[dev-dependencies] +pretty_assertions = "1" +rand = "0.8.5" +tokio = { version = "1", features = ["full"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5178fdb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2022 Christian Visintin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..9719a1d --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,56 @@ +[env] +CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true + +[tasks.build] +description = "Build canisters" +dependencies = [] +command = "dfx" +args = ["build"] +workspace = false + +[tasks.run] +description = "Run app" +dependencies = [] +command = "cargo" +args = ["run"] +workspace = false + +[tasks.test] +description = "Run unit tests" +command = "cargo" +args = ["test", "--lib"] +dependencies = ["did"] +workspace = false + + +[tasks.lint] +description = "Run lint" +command = "cargo" +args = ["clippy", "--", "-Dwarnings"] +workspace = false + +[tasks.format] +description = "Run the cargo rustfmt plugin." +command = "cargo" +args = ["fmt", "--all"] + +[tasks.check-format] +description = "Run the cargo rustfmt plugin." +command = "cargo" +args = ["fmt", "--all", "--", "--check"] + +[tasks.did] +description = "Generate did files" +script = "cargo run --bin icrc2-template-canister-did --features did > src/icrc2-template-canister.did" +workspace = false + +[tasks.dfx-generate] +description = "Generate dfx did" +command = "dfx" +args = ["generate"] +workspace = false + +[tasks.dfx-setup] +description = "setup dfx" +script = "dfx stop; dfx start --background; dfx canister create icrc2-template-canister" +workspace = false diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b0501b --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# ICRC2 Template Canister + +[![license-mit](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/license/mit/) +[![build-test](https://github.com/veeso-dev/icrc2-template-canister/actions/workflows/build-test.yml/badge.svg)](https://github.com/veeso-dev/icrc2-template-canister/actions/workflows/build-test.yml) +[![downloads](https://img.shields.io/crates/d/icrc2-template-canister.svg)](https://crates.io/crates/icrc2-template-canister) +[![latest version](https://img.shields.io/crates/v/icrc2-template-canister.svg)](https://crates.io/crates/icrc2-template-canister) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white)](https://conventionalcommits.org) + +--- + +- [ICRC2 Template Canister](#icrc2-template-canister) + - [About icrc2-template-canister](#about-icrc2-template-canister) + - [Contributing and issues](#contributing-and-issues) + - [License](#license) + +--- + +## About icrc2-template-canister + +This template provides a ready-to-use template to implement a ICRC2 token. All the functionalities expected to be available by the ICRC specs are already implemented. + +The produced wasm artifact can also be used for testing in pocket-ic if you for example need to interact with a dummy token. + +--- + +## Contributing and issues + +Contributions, bug reports, new features and questions are welcome! 😉 +If you have any question or concern, or you want to suggest a new feature, or you want just want to improve pavao, feel free to open an issue or a PR. + +Please follow [our contributing guidelines](CONTRIBUTING.md) + +--- + +## License + +icrc2-template-canister is licensed under the MIT license. + +You can read the entire license [HERE](LICENSE) diff --git a/dfx.json b/dfx.json new file mode 100644 index 0000000..18cf190 --- /dev/null +++ b/dfx.json @@ -0,0 +1,18 @@ +{ + "canisters": { + "icrc2-template-canister": { + "candid": "src/icrc2-template-canister.did", + "package": "icrc2_template_canister", + "type": "rust" + } + }, + "defaults": { + "build": { + "args": "", + "packtool": "" + } + }, + + "output_env_file": ".env", + "version": 1 +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3a3f3f1 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +imports_granularity = "Module" +group_imports = "StdExternalCrate" diff --git a/scripts/deploy_functions.sh b/scripts/deploy_functions.sh new file mode 100644 index 0000000..d643eed --- /dev/null +++ b/scripts/deploy_functions.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e + +deploy_canister() { + INSTALL_MODE="$1" + NETWORK="$2" + PRINCIPAL="$3" + NAME="$4" + SYMBOL="$5" + LOGO="$6" + FEE="$7" + DECIMALS="$8" + TOTAL_SUPPLY="$9" + ACCOUNTS="${10}" + MINTING_ACCOUNT="${11}" + + echo "deploying $NAME canister $PRINCIPAL" + + init_args="(record { + admins = vec { $(for admin in $ADMINS; do echo "principal \"$admin\";"; done) }; + total_supply = $TOTAL_SUPPLY; + accounts = $ACCOUNTS; + minting_account = $MINTING_ACCOUNT; + name = \"$NAME\"; + symbol = \"$SYMBOL\"; + logo = \"$LOGO\"; + fee = $FEE; + decimals = $DECIMALS; + })" + + dfx deploy --mode=$INSTALL_MODE --yes --network="$NETWORK" --argument="$init_args" icrc2-template + +} diff --git a/scripts/deploy_local.sh b/scripts/deploy_local.sh new file mode 100755 index 0000000..bb05977 --- /dev/null +++ b/scripts/deploy_local.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +cd "$(dirname "$0")" || exit 1 + +CANISTER_IDS="../.dfx/local/canister_ids.json" +PRINCIPAL="$(cat "$CANISTER_IDS" | jq -r '.icrc2-template-canister.local')" + +source ./deploy_functions.sh +source ./did.sh + +ADMIN_PRINCIPAL="$(dfx identity get-principal)" +TOTAL_SUPPLY="8880101010000000000" +ACCOUNTS="$(balances "$ADMIN_PRINCIPAL:250000000000000000")" +MINTING_ACCOUNT="$(account "$ADMIN_PRINCIPAL" "{33;169;149;73;231;146;144;124;94;39;94;84;81;6;141;173;223;77;67;238;141;202;180;135;86;35;26;143;183;113;49;35}")" + +dfx stop +dfx start --background + +cd ../ + +deploy_canister "reinstall" "local" \ + "$PRINCIPAL" \ + "MyToken" \ + "MYT" \ + "https://raw.githubusercontent.com/dfinity-lab/token-registry/main/assets/dfinity.png" \ + "0" \ + "8" \ + "$TOTAL_SUPPLY" \ + "$ACCOUNTS" \ + "$MINTING_ACCOUNT" + +dfx stop + +exit $RES diff --git a/scripts/did.sh b/scripts/did.sh new file mode 100644 index 0000000..e3767ad --- /dev/null +++ b/scripts/did.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +account() { + OWNER="$1" + SUBACCOUNT="$2" + + if [ -z "$SUBACCOUNT" ]; then + echo "record { owner = principal \"$OWNER\"; }" + else + echo "record { owner = principal \"$OWNER\"; subaccount = opt vec $SUBACCOUNT; }" + fi +} + +balances() { + BALANCES="$1" + + if [ -z "$BALANCES" ]; then + echo "vec {}" + else + echo "vec { $(for balance in $BALANCES; do echo "record { $(account ${balance%%:*}); ${balance##*:} };"; done) }" + fi +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..498ad5d --- /dev/null +++ b/src/app.rs @@ -0,0 +1,848 @@ +//! # App +//! +//! API implementation for deferred canister + +mod balance; +mod configuration; +mod inspect; +mod memory; +mod spend_allowance; +#[cfg(test)] +mod test_utils; + +use candid::{CandidType, Nat}; +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::{self, transfer as icrc1_transfer}; +use icrc_ledger_types::icrc2; +use serde::Deserialize; +use thiserror::Error; + +use self::balance::Balance; +use self::configuration::Configuration; +pub use self::inspect::Inspect; +use self::spend_allowance::SpendAllowance; +use crate::utils::{self, caller}; +use crate::InitArgs; + +pub type CanisterResult = Result; + +#[derive(Clone, Debug, Error, CandidType, PartialEq, Eq, Deserialize)] +pub enum CanisterError { + #[error("allowance error {0}")] + Allowance(AllowanceError), + #[error("balance error {0}")] + Balance(BalanceError), + #[error("configuration error {0}")] + Configuration(ConfigurationError), + #[error("storage error")] + StorageError, + #[error("icrc2 transfer error {0:?}")] + Icrc2Transfer(icrc2::transfer_from::TransferFromError), + #[error("icrc1 transfer error {0:?}")] + Icrc1Transfer(icrc1::transfer::TransferError), +} + +impl From for CanisterError { + fn from(value: icrc2::transfer_from::TransferFromError) -> Self { + Self::Icrc2Transfer(value) + } +} + +impl From for CanisterError { + fn from(value: icrc1::transfer::TransferError) -> Self { + Self::Icrc1Transfer(value) + } +} + +#[derive(Clone, Debug, Error, CandidType, PartialEq, Eq, Deserialize)] +pub enum AllowanceError { + #[error("allowance not found")] + AllowanceNotFound, + #[error("allowance changed")] + AllowanceChanged, + #[error("allowance expired")] + AllowanceExpired, + #[error("the spender cannot be the caller")] + BadSpender, + #[error("the expiration date is in the past")] + BadExpiration, + #[error("insufficient funds")] + InsufficientFunds, +} + +#[derive(Clone, Debug, Error, CandidType, PartialEq, Eq, Deserialize)] +pub enum BalanceError { + #[error("account not found")] + AccountNotFound, + #[error("insufficient balance")] + InsufficientBalance, +} + +#[derive(Clone, Debug, Error, CandidType, PartialEq, Eq, Deserialize)] +pub enum ConfigurationError { + #[error("there must be at least one admin")] + AdminsCantBeEmpty, + #[error("the canister admin cannot be anonymous")] + AnonymousAdmin, +} + +pub struct Icrc2Canister; + +impl Icrc2Canister { + /// Init fly canister + pub fn init(data: InitArgs) { + // Set minting account + Configuration::set_minting_account(data.minting_account); + // set token data + Configuration::set_decimals(data.decimals); + Configuration::set_fee(data.fee); + Configuration::set_name(data.name); + Configuration::set_symbol(data.symbol); + Configuration::set_logo(data.logo); + // init balances + Balance::init_balances(data.total_supply, data.accounts); + // set timers + Self::set_timers(); + } + + pub fn post_upgrade() { + Self::set_timers(); + } + + /// Set application timers + fn set_timers() { + #[cfg(target_family = "wasm")] + ic_cdk_timers::set_timer_interval( + crate::constants::SPEND_ALLOWANCE_EXPIRED_ALLOWANCE_TIMER_INTERVAL, + SpendAllowance::remove_expired_allowance, + ); + } + + /// Returns cycles + pub fn cycles() -> Nat { + utils::cycles() + } + + pub fn icrc1_name() -> String { + Configuration::get_name() + } + + pub fn icrc1_symbol() -> String { + Configuration::get_symbol() + } + + pub fn icrc1_decimals() -> u8 { + Configuration::get_decimals() + } + + pub fn icrc1_fee() -> Nat { + Configuration::get_fee().into() + } + + pub fn icrc1_metadata() -> Vec<(String, MetadataValue)> { + vec![ + ( + "icrc1:symbol".to_string(), + MetadataValue::from(Self::icrc1_symbol()), + ), + ( + "icrc1:name".to_string(), + MetadataValue::from(Self::icrc1_name()), + ), + ( + "icrc1:decimals".to_string(), + MetadataValue::from(Nat::from(Self::icrc1_decimals())), + ), + ( + "icrc1:fee".to_string(), + MetadataValue::from(Self::icrc1_fee()), + ), + ( + "icrc1:logo".to_string(), + MetadataValue::from(Configuration::get_logo()), + ), + ] + } + + pub fn icrc1_total_supply() -> Nat { + Balance::total_supply() + } + + pub fn icrc1_minting_account() -> Account { + Configuration::get_minting_account() + } + + pub fn icrc1_balance_of(account: Account) -> Nat { + Balance::balance_of(account).unwrap_or_default() + } + + pub fn icrc1_transfer( + transfer_args: icrc1_transfer::TransferArg, + ) -> Result { + // get fee and check if fee is at least ICRC1_FEE + Inspect::inspect_transfer(&transfer_args)?; + let fee = transfer_args.fee.unwrap_or(Self::icrc1_fee()); + + // get from account + let from_account = Account { + owner: utils::caller(), + subaccount: transfer_args.from_subaccount, + }; + + // check if it is a burn + if transfer_args.to == Self::icrc1_minting_account() { + Balance::transfer_wno_fees(from_account, transfer_args.to, transfer_args.amount.clone()) + } else { + // make transfer + Balance::transfer( + from_account, + transfer_args.to, + transfer_args.amount.clone(), + fee.clone(), + ) + } + .map_err(|err| match err { + CanisterError::Balance(BalanceError::InsufficientBalance) => { + icrc1_transfer::TransferError::InsufficientFunds { + balance: Self::icrc1_balance_of(from_account), + } + } + _ => icrc1_transfer::TransferError::GenericError { + error_code: Nat::from(3), + message: err.to_string(), + }, + })?; + + Ok(1.into()) + } + + pub fn icrc1_supported_standards() -> Vec { + vec![ + super::TokenExtension::icrc1(), + super::TokenExtension::icrc2(), + ] + } + + pub fn icrc2_approve( + args: icrc2::approve::ApproveArgs, + ) -> Result { + Inspect::inspect_icrc2_approve(caller(), &args)?; + + let caller_account = Account { + owner: caller(), + subaccount: args.from_subaccount, + }; + + let current_allowance = SpendAllowance::get_allowance(caller_account, args.spender).0; + + // pay fee + let fee = args.fee.clone().unwrap_or(Self::icrc1_fee()); + Balance::transfer_wno_fees(caller_account, Configuration::get_minting_account(), fee) + .map_err(|_| icrc2::approve::ApproveError::InsufficientFunds { + balance: Self::icrc1_balance_of(caller_account), + })?; + + // approve spend + match SpendAllowance::approve_spend(caller(), args) { + Ok(amount) => Ok(amount), + Err(CanisterError::Allowance(AllowanceError::AllowanceChanged)) => { + Err(icrc2::approve::ApproveError::AllowanceChanged { current_allowance }) + } + Err(CanisterError::Allowance(AllowanceError::BadExpiration)) => { + Err(icrc2::approve::ApproveError::TooOld) + } + Err(err) => Err(icrc2::approve::ApproveError::GenericError { + error_code: 0.into(), + message: err.to_string(), + }), + } + } + + pub fn icrc2_transfer_from( + args: icrc2::transfer_from::TransferFromArgs, + ) -> Result { + Inspect::inspect_icrc2_transfer_from(&args)?; + + // check if owner has enough balance + let owner_balance = Self::icrc1_balance_of(args.from); + if owner_balance < args.amount { + return Err(icrc2::transfer_from::TransferFromError::InsufficientFunds { + balance: owner_balance, + }); + } + + // check if spender has fee + let spender = Account { + owner: caller(), + subaccount: args.spender_subaccount, + }; + let spender_balance = Self::icrc1_balance_of(spender); + let fee = args.fee.clone().unwrap_or(Self::icrc1_fee()); + if spender_balance < fee { + return Err(icrc2::transfer_from::TransferFromError::InsufficientFunds { + balance: spender_balance, + }); + } + + // check allowance + let (allowance, expires_at) = SpendAllowance::get_allowance(args.from, spender); + if allowance < args.amount { + return Err( + icrc2::transfer_from::TransferFromError::InsufficientAllowance { allowance }, + ); + } + + // check if has expired + if expires_at.is_some() && expires_at.unwrap() < utils::time() { + return Err(icrc2::transfer_from::TransferFromError::TooOld); + } + + // spend allowance + match SpendAllowance::spend_allowance( + caller(), + args.from, + args.amount.clone(), + args.spender_subaccount, + ) { + Ok(()) => Ok(()), + Err(CanisterError::Allowance(AllowanceError::InsufficientFunds)) => { + Err(icrc2::transfer_from::TransferFromError::InsufficientAllowance { allowance }) + } + Err(CanisterError::Allowance(AllowanceError::AllowanceExpired)) => { + Err(icrc2::transfer_from::TransferFromError::TooOld) + } + Err(e) => Err(icrc2::transfer_from::TransferFromError::GenericError { + error_code: 0.into(), + message: e.to_string(), + }), + }?; + + // pay fee + Balance::transfer_wno_fees(spender, Configuration::get_minting_account(), fee.clone()) + .map_err( + |_| icrc2::transfer_from::TransferFromError::InsufficientFunds { + balance: Self::icrc1_balance_of(spender), + }, + )?; + + // transfer from `from` balance to `to` balance + Balance::transfer_wno_fees(args.from, args.to, args.amount.clone()).map_err(|_| { + icrc2::transfer_from::TransferFromError::InsufficientFunds { + balance: Self::icrc1_balance_of(args.from), + } + })?; + + // register transaction + Ok(1.into()) + } + + pub fn icrc2_allowance(args: icrc2::allowance::AllowanceArgs) -> icrc2::allowance::Allowance { + let (allowance, expires_at) = SpendAllowance::get_allowance(args.account, args.spender); + icrc2::allowance::Allowance { + allowance, + expires_at, + } + } +} + +#[cfg(test)] +mod test { + + use icrc_ledger_types::icrc1::transfer::TransferArg; + use icrc_ledger_types::icrc2::allowance::{Allowance, AllowanceArgs}; + use icrc_ledger_types::icrc2::approve::ApproveArgs; + use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; + use pretty_assertions::assert_eq; + + use self::test_utils::minting_account; + use super::test_utils::{alice_account, bob_account, caller_account, int_to_decimals}; + use super::*; + use crate::app::test_utils::bob; + use crate::constants::ICRC1_TX_TIME_SKID; + + const ICRC1_NAME: &str = "dummy"; + /// Token symbol + const ICRC1_SYMBOL: &str = "DUM"; + /// pico fly + const ICRC1_DECIMALS: u8 = 12; + /// Default transfer fee (10.000 picofly) + const ICRC1_FEE: u64 = 10_000; + /// Logo + const ICRC1_LOGO: &str = ""; + + #[tokio::test] + async fn test_should_init_canister() { + init_canister(); + + // init balance + assert_eq!( + Balance::balance_of(alice_account()).unwrap(), + int_to_decimals(50_000) + ); + assert_eq!( + Balance::balance_of(bob_account()).unwrap(), + int_to_decimals(50_000) + ); + assert_eq!( + Balance::balance_of(caller_account()).unwrap(), + int_to_decimals(100_000) + ); + } + + #[tokio::test] + async fn test_should_get_name() { + init_canister(); + assert_eq!(Icrc2Canister::icrc1_name(), ICRC1_NAME); + } + + #[tokio::test] + async fn test_should_get_symbol() { + init_canister(); + assert_eq!(Icrc2Canister::icrc1_symbol(), ICRC1_SYMBOL); + } + + #[tokio::test] + async fn test_should_get_decimals() { + init_canister(); + assert_eq!(Icrc2Canister::icrc1_decimals(), ICRC1_DECIMALS); + } + + #[tokio::test] + async fn test_should_get_fee() { + init_canister(); + assert_eq!(Icrc2Canister::icrc1_fee(), Nat::from(ICRC1_FEE)); + } + + #[tokio::test] + async fn test_should_get_metadata() { + init_canister(); + let metadata = Icrc2Canister::icrc1_metadata(); + assert_eq!(metadata.len(), 5); + assert_eq!( + metadata.get(0).unwrap(), + &( + "icrc1:symbol".to_string(), + MetadataValue::from(ICRC1_SYMBOL) + ) + ); + assert_eq!( + metadata.get(1).unwrap(), + &("icrc1:name".to_string(), MetadataValue::from(ICRC1_NAME)) + ); + assert_eq!( + metadata.get(2).unwrap(), + &( + "icrc1:decimals".to_string(), + MetadataValue::from(Nat::from(ICRC1_DECIMALS)) + ) + ); + assert_eq!( + metadata.get(3).unwrap(), + &( + "icrc1:fee".to_string(), + MetadataValue::from(Nat::from(ICRC1_FEE)) + ) + ); + assert_eq!( + metadata.get(4).unwrap(), + &("icrc1:logo".to_string(), MetadataValue::from(ICRC1_LOGO)) + ); + } + + #[tokio::test] + async fn test_should_get_total_supply() { + init_canister(); + assert_eq!( + Icrc2Canister::icrc1_total_supply(), + Nat::from(int_to_decimals(8_888_888)) + ); + } + + #[tokio::test] + async fn test_should_get_minting_account() { + init_canister(); + assert_eq!( + Icrc2Canister::icrc1_minting_account(), + Configuration::get_minting_account() + ); + } + + #[tokio::test] + async fn test_should_get_balance_of() { + init_canister(); + assert_eq!( + Icrc2Canister::icrc1_balance_of(alice_account()), + Nat::from(int_to_decimals(50_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(bob_account()), + Nat::from(int_to_decimals(50_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(100_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(Account { + owner: utils::id(), + subaccount: Some(utils::random_subaccount().await), + }), + Nat::from(0) + ); + } + + #[tokio::test] + async fn test_should_transfer() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE)), + created_at_time: Some(utils::time()), + memo: None, + }; + assert!(Icrc2Canister::icrc1_transfer(transfer_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(90_000) - ICRC1_FEE) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(bob_account()), + Nat::from(int_to_decimals(60_000)) + ); + } + + #[tokio::test] + async fn test_should_not_transfer_with_bad_time() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE)), + created_at_time: Some(0), + memo: None, + }; + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::TooOld { .. } + )); + } + + #[tokio::test] + async fn test_should_not_transfer_with_old_time() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE)), + created_at_time: Some(utils::time() - (ICRC1_TX_TIME_SKID.as_nanos() as u64 * 2)), + memo: None, + }; + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::TooOld { .. } + )); + } + + #[tokio::test] + async fn test_should_not_transfer_with_time_in_future() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE)), + created_at_time: Some(utils::time() + (ICRC1_TX_TIME_SKID.as_nanos() as u64 * 2)), + memo: None, + }; + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::CreatedInFuture { .. } + )); + } + + #[tokio::test] + async fn test_should_not_transfer_with_bad_fee() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE / 2)), + created_at_time: Some(utils::time()), + memo: None, + }; + + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::BadFee { .. } + )); + } + + #[tokio::test] + async fn test_should_transfer_with_null_fee() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + created_at_time: Some(utils::time()), + memo: None, + }; + assert!(Icrc2Canister::icrc1_transfer(transfer_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(90_000) - ICRC1_FEE) + ); + } + + #[tokio::test] + async fn test_should_transfer_with_higher_fee() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE * 2)), + created_at_time: Some(utils::time()), + memo: None, + }; + assert!(Icrc2Canister::icrc1_transfer(transfer_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(90_000) - (ICRC1_FEE * 2)) + ); + } + + #[tokio::test] + async fn test_should_not_allow_bad_memo() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + created_at_time: Some(utils::time()), + memo: Some("9888".as_bytes().to_vec().into()), + }; + + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::GenericError { .. } + )); + + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + created_at_time: Some(utils::time()), + memo: Some("988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888".as_bytes().to_vec().into()), + }; + + assert!(matches!( + Icrc2Canister::icrc1_transfer(transfer_args).unwrap_err(), + icrc1_transfer::TransferError::GenericError { .. } + )); + } + + #[tokio::test] + async fn test_should_transfer_with_memo() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(ICRC1_FEE)), + created_at_time: Some(utils::time()), + memo: Some( + "293458234690283506958436839246024563" + .to_string() + .as_bytes() + .to_vec() + .into(), + ), + }; + assert!(Icrc2Canister::icrc1_transfer(transfer_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(90_000) - ICRC1_FEE) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(bob_account()), + Nat::from(int_to_decimals(60_000)) + ); + } + + #[tokio::test] + async fn test_should_burn_from_transfer() { + init_canister(); + let transfer_args = TransferArg { + from_subaccount: caller_account().subaccount, + to: Icrc2Canister::icrc1_minting_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + created_at_time: Some(utils::time()), + memo: None, + }; + assert!(Icrc2Canister::icrc1_transfer(transfer_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(90_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_total_supply(), + Nat::from(int_to_decimals(8_888_888 - 10_000)) + ); + } + + #[tokio::test] + async fn test_should_get_supported_extensions() { + init_canister(); + let extensions = Icrc2Canister::icrc1_supported_standards(); + assert_eq!(extensions.len(), 2); + assert_eq!( + extensions.get(0).unwrap().name, + crate::TokenExtension::icrc1().name + ); + assert_eq!( + extensions.get(1).unwrap().name, + crate::TokenExtension::icrc2().name + ); + } + + #[tokio::test] + async fn test_should_approve_spending() { + init_canister(); + let approval_args = ApproveArgs { + from_subaccount: caller_account().subaccount, + spender: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + expires_at: None, + expected_allowance: None, + memo: None, + created_at_time: None, + }; + + assert!(Icrc2Canister::icrc2_approve(approval_args).is_ok()); + // check allowance + assert_eq!( + Icrc2Canister::icrc2_allowance(AllowanceArgs { + account: caller_account(), + spender: bob_account(), + }), + Allowance { + allowance: Nat::from(int_to_decimals(10_000)), + expires_at: None, + } + ); + // check we have paid fee + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + int_to_decimals(100_000) - ICRC1_FEE + ); + } + + #[tokio::test] + async fn test_should_not_approve_spending_if_we_cannot_pay_fee() { + init_canister(); + let approval_args = ApproveArgs { + from_subaccount: caller_account().subaccount, + spender: bob_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: Some(Nat::from(int_to_decimals(110_000))), + expires_at: None, + expected_allowance: None, + memo: None, + created_at_time: None, + }; + + assert!(Icrc2Canister::icrc2_approve(approval_args).is_err()); + } + + #[tokio::test] + async fn test_should_spend_approved_amount() { + init_canister(); + let approval_args = ApproveArgs { + from_subaccount: bob_account().subaccount, + spender: caller_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + expires_at: None, + expected_allowance: None, + memo: None, + created_at_time: None, + }; + assert!(SpendAllowance::approve_spend(bob(), approval_args).is_ok()); + assert_eq!( + Icrc2Canister::icrc2_allowance(AllowanceArgs { + account: bob_account(), + spender: caller_account(), + }), + Allowance { + allowance: Nat::from(int_to_decimals(10_000)), + expires_at: None, + } + ); + + // spend + assert!(Icrc2Canister::icrc2_transfer_from(TransferFromArgs { + spender_subaccount: caller_account().subaccount, + from: bob_account(), + to: alice_account(), + amount: Nat::from(int_to_decimals(10_000)), + fee: None, + memo: None, + created_at_time: None, + }) + .is_ok()); + // verify balance + assert_eq!( + Icrc2Canister::icrc1_balance_of(bob_account()), + Nat::from(int_to_decimals(40_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(alice_account()), + Nat::from(int_to_decimals(60_000)) + ); + assert_eq!( + Icrc2Canister::icrc1_balance_of(caller_account()), + Nat::from(int_to_decimals(100_000) - ICRC1_FEE) + ); + // verify allowance + assert_eq!( + Icrc2Canister::icrc2_allowance(AllowanceArgs { + account: bob_account(), + spender: caller_account(), + }), + Allowance { + allowance: Nat::from(int_to_decimals(0)), + expires_at: None, + } + ); + } + + fn init_canister() { + let data = InitArgs { + accounts: vec![ + (alice_account(), int_to_decimals(50_000)), + (bob_account(), int_to_decimals(50_000)), + (caller_account(), int_to_decimals(100_000)), + ], + symbol: ICRC1_SYMBOL.to_string(), + name: ICRC1_NAME.to_string(), + decimals: ICRC1_DECIMALS, + fee: ICRC1_FEE.into(), + logo: ICRC1_LOGO.to_string(), + total_supply: int_to_decimals(8_888_888), + minting_account: minting_account(), + }; + Icrc2Canister::init(data); + } +} diff --git a/src/app/balance.rs b/src/app/balance.rs new file mode 100644 index 0000000..71de538 --- /dev/null +++ b/src/app/balance.rs @@ -0,0 +1,336 @@ +//! # Balances +//! +//! ICRC-1 token balances + +mod account_balance; + +use std::cell::RefCell; + +use candid::{Nat, Principal}; +use ic_stable_structures::memory_manager::VirtualMemory; +use ic_stable_structures::{DefaultMemoryImpl, StableBTreeMap, StableCell}; +use icrc_ledger_types::icrc1::account::Account; +use num_bigint::BigUint; + +use self::account_balance::Balance as AccountBalance; +use super::configuration::Configuration; +use super::{BalanceError, CanisterError, CanisterResult}; +use crate::app::memory::{ + StorableAccount, BALANCES_MEMORY_ID, CANISTER_WALLET_ACCOUNT_MEMORY_ID, MEMORY_MANAGER, +}; + +thread_local! { + /// Account balances + static BALANCES: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(BALANCES_MEMORY_ID))) + ); + + /// Wallet which contains all the native tokens of the canister + static CANISTER_WALLET_ACCOUNT: RefCell>> = RefCell::new( + StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(CANISTER_WALLET_ACCOUNT_MEMORY_ID)), + Account { + owner: Principal::anonymous(), + subaccount: None, + }.into()).unwrap()); +} + +pub struct Balance; + +impl Balance { + /// Set init balances + /// + /// WARNING: this function DOESN'T check anything and it's meant to be used only on init. + /// Panics if initializing more than total supply. + pub fn init_balances(total_supply: Nat, initial_balances: Vec<(Account, Nat)>) { + // make canister acount + let canister_account = Account { + owner: crate::utils::id(), + subaccount: None, + }; + // set canister + CANISTER_WALLET_ACCOUNT.with_borrow_mut(|wallet| { + wallet + .set(StorableAccount::from(canister_account)) + .expect("failed to set canister account"); + }); + + BALANCES.with_borrow_mut(|balances| { + let canister_balance = + Nat(total_supply.0 - initial_balances.iter().map(|(_, b)| &b.0).sum::()); + // init accounts + for (account, balance) in initial_balances { + let storable_account = StorableAccount::from(account); + balances.insert(storable_account, balance.clone().into()); + } + // set remaining supply to canister account + balances.insert( + StorableAccount::from(canister_account), + AccountBalance { + amount: canister_balance, + }, + ); + }); + } + + pub fn total_supply() -> Nat { + let minting_account = Configuration::get_minting_account(); + BALANCES.with_borrow(|balances| { + let mut supply = Nat::from(0); + for (account, balance) in balances.iter() { + if minting_account != account.0 { + supply += balance.amount; + } + } + + supply + }) + } + + /// Get balance of account + pub fn balance_of(account: Account) -> CanisterResult { + Self::with_balance(account, |balance| balance.amount.clone()) + } + + /// Transfer $picoFly tokens from `from` account to `to` account. + /// The fee is transferred to the Minting Account, making it burned + pub fn transfer(from: Account, to: Account, value: Nat, fee: Nat) -> CanisterResult<()> { + // verify balance + let to_spend = value.clone() + fee.clone(); + if Self::balance_of(from)? < to_spend { + return Err(CanisterError::Balance(BalanceError::InsufficientBalance)); + } + + // transfer without fees from -> to + Self::transfer_wno_fees(from, to, value)?; + + // then pay fees + if fee > 0_u64 { + Self::transfer_wno_fees(from, Configuration::get_minting_account(), fee) + } else { + Ok(()) + } + } + + /// Transfer $picoFly tokens from canister to `to` account. + /// + /// This function is meant to be used only by the deferred canister and does not apply fees or burns. + pub fn transfer_wno_fees(from: Account, to: Account, value: Nat) -> CanisterResult<()> { + Self::with_balance_mut(from, |balance| { + if balance.amount < value { + return Err(CanisterError::Balance(BalanceError::InsufficientBalance)); + } + balance.amount -= value.clone(); + Ok(()) + })?; + Self::with_balance_mut(to, |balance| { + balance.amount += value; + Ok(()) + }) + } + + fn with_balance(account: Account, f: F) -> CanisterResult + where + F: FnOnce(&AccountBalance) -> T, + { + let storable_account = StorableAccount::from(account); + BALANCES.with_borrow(|balances| match balances.get(&storable_account) { + Some(balance) => Ok(f(&balance)), + None => Err(CanisterError::Balance(BalanceError::AccountNotFound)), + }) + } + + fn with_balance_mut(account: Account, f: F) -> CanisterResult + where + F: FnOnce(&mut AccountBalance) -> CanisterResult, + { + let storable_account = StorableAccount::from(account); + BALANCES.with_borrow_mut(|balances| { + let mut balance = match balances.get(&storable_account) { + Some(balance) => balance, + None => { + // If balance is not set, create it with 0 balance + balances.insert(storable_account.clone(), AccountBalance::from(Nat::from(0))); + balances.get(&storable_account).unwrap() + } + }; + let res = f(&mut balance)?; + + balances.insert(storable_account, balance); + + Ok(res) + }) + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::{alice_account, bob_account, int_to_decimals}; + use crate::utils::{self}; + + #[test] + fn test_should_init_balances() { + let total_supply = int_to_decimals(8_888_888); + + let initial_balances = vec![ + (alice_account(), int_to_decimals(188_888)), + (bob_account(), int_to_decimals(100_000)), + ]; + + Balance::init_balances(total_supply, initial_balances); + + let canister_account = CANISTER_WALLET_ACCOUNT.with_borrow(|wallet| wallet.get().0.clone()); + assert_eq!( + Balance::balance_of(canister_account).unwrap(), + int_to_decimals(8_888_888 - 188_888 - 100_000) + ); + + assert_eq!( + Balance::balance_of(alice_account()).unwrap(), + int_to_decimals(188_888) + ); + assert_eq!( + Balance::balance_of(bob_account()).unwrap(), + int_to_decimals(100_000) + ); + } + + #[tokio::test] + async fn test_should_transfer_from_canister() { + let total_supply = int_to_decimals(8_888_888); + let recipient_account = Account { + owner: utils::id(), + subaccount: Some(utils::random_subaccount().await), + }; + + let initial_balances = vec![(recipient_account.clone(), int_to_decimals(888))]; + + Balance::init_balances(total_supply, initial_balances); + + assert_eq!( + Balance::balance_of(recipient_account).unwrap(), + int_to_decimals(888) + ); + } + + #[test] + fn test_should_transfer_between_accounts() { + let total_supply = int_to_decimals(8_888_888); + let initial_balances = vec![ + (alice_account(), int_to_decimals(120)), + (bob_account(), int_to_decimals(50)), + ]; + Balance::init_balances(total_supply, initial_balances); + + // transfer + assert!(Balance::transfer( + alice_account(), + bob_account(), + int_to_decimals(50), + int_to_decimals(1) + ) + .is_ok()); + // verify balances + assert_eq!( + Balance::balance_of(alice_account()).unwrap(), + int_to_decimals(120 - 50 - 1) + ); + assert_eq!( + Balance::balance_of(bob_account()).unwrap(), + int_to_decimals(100) + ); + // fee should be burned + assert_eq!(Balance::total_supply(), int_to_decimals(8_888_888 - 1)); + } + + #[test] + fn test_should_fail_transfer_if_has_no_balance_to_pay_fee() { + let total_supply = int_to_decimals(8_888_888); + let initial_balances = vec![ + (alice_account(), int_to_decimals(50)), + (bob_account(), int_to_decimals(50)), + ]; + Balance::init_balances(total_supply, initial_balances); + + // transfer + assert!(Balance::transfer( + alice_account(), + bob_account(), + int_to_decimals(50), + int_to_decimals(1) + ) + .is_err()); + } + + #[test] + fn test_should_not_pay_fee_if_fee_is_zero() { + let total_supply = int_to_decimals(8_888_888); + let initial_balances = vec![ + (alice_account(), int_to_decimals(50)), + (bob_account(), int_to_decimals(50)), + ]; + Balance::init_balances(total_supply, initial_balances); + + // transfer + assert!(Balance::transfer( + alice_account(), + bob_account(), + int_to_decimals(50), + int_to_decimals(0) + ) + .is_ok()); + // verify balances + assert_eq!( + Balance::balance_of(alice_account()).unwrap(), + int_to_decimals(0) + ); + assert_eq!( + Balance::balance_of(bob_account()).unwrap(), + int_to_decimals(100) + ); + // fee should be burned + assert_eq!(Balance::total_supply(), int_to_decimals(8_888_888)); + } + + #[test] + fn test_should_not_allow_transfer_if_not_enough_balance() { + let total_supply = int_to_decimals(8_888_888); + let initial_balances = vec![ + (alice_account(), int_to_decimals(50)), + (bob_account(), int_to_decimals(50)), + ]; + Balance::init_balances(total_supply, initial_balances); + + // transfer + assert!(Balance::transfer( + alice_account(), + bob_account(), + int_to_decimals(100), + int_to_decimals(1) + ) + .is_err()); + } + + #[test] + fn test_should_get_total_supply() { + let total_supply = int_to_decimals(8_888_888); + let initial_balances = vec![(bob_account(), int_to_decimals(100_000))]; + Balance::init_balances(total_supply, initial_balances); + assert_eq!(Balance::total_supply(), int_to_decimals(8_888_888)); + + // burn + assert!(Balance::transfer_wno_fees( + bob_account(), + Configuration::get_minting_account(), + int_to_decimals(100_000) + ) + .is_ok()); + assert_eq!( + Balance::total_supply(), + int_to_decimals(8_888_888 - 100_000) + ); + } +} diff --git a/src/app/balance/account_balance.rs b/src/app/balance/account_balance.rs new file mode 100644 index 0000000..42725d9 --- /dev/null +++ b/src/app/balance/account_balance.rs @@ -0,0 +1,65 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use candid::Nat; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::Storable; +use num_bigint::BigUint; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// Describes the balance of an account +pub struct Balance { + pub amount: Nat, +} + +impl From for Balance { + fn from(amount: Nat) -> Self { + Self { amount } + } +} + +impl Storable for Balance { + const BOUND: Bound = Bound::Bounded { + max_size: 64, + is_fixed_size: false, + }; + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + let mut bytes = Bytes::from(bytes.to_vec()); + + let amount_len = bytes.get_u8(); + + bytes.slice(..amount_len as usize); + let amount = BigUint::from_bytes_be(&bytes.slice(..amount_len as usize)).into(); + + Self { amount } + } + + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + let mut bytes = BytesMut::with_capacity(Self::BOUND.max_size() as usize); + let amount_bytes = self.amount.0.to_bytes_be(); + bytes.put_u8(amount_bytes.len() as u8); + bytes.put(self.amount.0.to_bytes_be().as_slice()); + + bytes.to_vec().into() + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::int_to_decimals; + + #[test] + fn test_should_encode_and_decode_balance() { + let balance = Balance { + amount: int_to_decimals(8_888_888), + }; + + let encoded = balance.to_bytes(); + let decoded = Balance::from_bytes(encoded); + + assert_eq!(balance, decoded); + } +} diff --git a/src/app/configuration.rs b/src/app/configuration.rs new file mode 100644 index 0000000..374a1e8 --- /dev/null +++ b/src/app/configuration.rs @@ -0,0 +1,123 @@ +//! # Configuration +//! +//! Canister configuration + +use std::cell::RefCell; + +use candid::Principal; +use ic_stable_structures::memory_manager::VirtualMemory; +use ic_stable_structures::{DefaultMemoryImpl, StableCell}; +use icrc_ledger_types::icrc1::account::Account; + +use super::memory::StorableAccount; +use crate::app::memory::{ + DECIMALS_MEMORY_ID, FEE_MEMORY_ID, LOGO_MEMORY_ID, MEMORY_MANAGER, MINTING_ACCOUNT_MEMORY_ID, + NAME_MEMORY_ID, SYMBOL_MEMORY_ID, +}; + +thread_local! { + /// Minting account + static MINTING_ACCOUNT: RefCell>> = + RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(MINTING_ACCOUNT_MEMORY_ID)), + Account { + owner: Principal::anonymous(), + subaccount: None + }.into()).unwrap() + ); + + static NAME: RefCell>> = + RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(NAME_MEMORY_ID)), "ICRC-2".to_string()).unwrap()); + + static SYMBOL: RefCell>> = RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(SYMBOL_MEMORY_ID)), "ICRC".to_string()).unwrap()); + + static DECIMALS: RefCell>> = RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(DECIMALS_MEMORY_ID)), 8).unwrap()); + + static FEE: RefCell>> = RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(FEE_MEMORY_ID)), 0).unwrap()); + + static LOGO: RefCell>> = RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(LOGO_MEMORY_ID)), "".to_string()).unwrap()); + + +} + +/// canister configuration +pub struct Configuration; + +impl Configuration { + /// Set minting account + pub fn set_minting_account(minting_account: Account) { + MINTING_ACCOUNT.with_borrow_mut(|cell| { + cell.set(minting_account.into()).unwrap(); + }); + } + + /// Get minting account address + pub fn get_minting_account() -> Account { + MINTING_ACCOUNT.with(|ma| ma.borrow().get().0) + } + + pub fn set_name(name: String) { + NAME.with_borrow_mut(|cell| { + cell.set(name).unwrap(); + }); + } + + pub fn get_name() -> String { + NAME.with(|name| name.borrow().get().clone()) + } + + pub fn set_symbol(symbol: String) { + SYMBOL.with_borrow_mut(|cell| { + cell.set(symbol).unwrap(); + }); + } + + pub fn get_symbol() -> String { + SYMBOL.with(|symbol| symbol.borrow().get().clone()) + } + + pub fn set_decimals(decimals: u8) { + DECIMALS.with_borrow_mut(|cell| { + cell.set(decimals).unwrap(); + }); + } + + pub fn get_decimals() -> u8 { + DECIMALS.with(|decimals| decimals.borrow().get().clone()) + } + + pub fn set_fee(fee: u64) { + FEE.with_borrow_mut(|cell| { + cell.set(fee).unwrap(); + }); + } + + pub fn get_fee() -> u64 { + FEE.with(|fee| fee.borrow().get().clone()) + } + + pub fn set_logo(logo: String) { + LOGO.with_borrow_mut(|cell| { + cell.set(logo).unwrap(); + }); + } + + pub fn get_logo() -> String { + LOGO.with(|logo| logo.borrow().get().clone()) + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::bob_account; + + #[test] + fn test_should_set_minting_account() { + let minting_account = bob_account(); + Configuration::set_minting_account(minting_account); + assert_eq!(Configuration::get_minting_account(), minting_account); + } +} diff --git a/src/app/inspect.rs b/src/app/inspect.rs new file mode 100644 index 0000000..5bdf788 --- /dev/null +++ b/src/app/inspect.rs @@ -0,0 +1,386 @@ +//! # Inspect +//! +//! Deferred inspect message handler + +use std::time::Duration; + +use candid::{Nat, Principal}; +use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError}; +use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError}; +use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError}; + +use super::configuration::Configuration; +use crate::constants::ICRC1_TX_TIME_SKID; +use crate::utils::time; + +pub struct Inspect; + +impl Inspect { + /// inspect whether transfer update is valid + pub fn inspect_transfer(args: &TransferArg) -> Result<(), TransferError> { + let default_fee: Nat = Configuration::get_fee().into(); + let fee = args.fee.clone().unwrap_or(default_fee.clone()); + if fee < default_fee { + return Err(TransferError::BadFee { + expected_fee: default_fee, + }); + } + + // check if the transaction is too old + let now = Duration::from_nanos(time()); + let tx_created_at = + Duration::from_nanos(args.created_at_time.unwrap_or(now.as_nanos() as u64)); + if now > tx_created_at && now.saturating_sub(tx_created_at) > ICRC1_TX_TIME_SKID { + return Err(TransferError::TooOld); + } else if tx_created_at.saturating_sub(now) > ICRC1_TX_TIME_SKID { + return Err(TransferError::CreatedInFuture { + ledger_time: now.as_nanos() as u64, + }); + } + + // check memo length + if let Some(memo) = &args.memo { + if memo.0.len() < 32 || memo.0.len() > 64 { + return Err(TransferError::GenericError { + error_code: Nat::from(1), + message: "Invalid memo length. I must have a length between 32 and 64 bytes" + .to_string(), + }); + } + } + + Ok(()) + } + + /// inspect icrc2 approve arguments + pub fn inspect_icrc2_approve( + caller: Principal, + args: &ApproveArgs, + ) -> Result<(), ApproveError> { + let default_fee: Nat = Configuration::get_fee().into(); + if args.spender.owner == caller { + return Err(ApproveError::GenericError { + error_code: 0_u64.into(), + message: "Spender and owner cannot be equal".to_string(), + }); + } + if args + .fee + .as_ref() + .map(|fee| fee < &default_fee) + .unwrap_or(false) + { + return Err(ApproveError::BadFee { + expected_fee: default_fee.into(), + }); + } + // check if expired + if args + .expires_at + .as_ref() + .map(|expiry| expiry < &time()) + .unwrap_or(false) + { + return Err(ApproveError::Expired { + ledger_time: time(), + }); + } + // check if too old or in the future + if let Some(created_at) = args.created_at_time { + let current_time = Duration::from_nanos(time()); + let created_at = Duration::from_nanos(created_at); + + if created_at > current_time { + return Err(ApproveError::CreatedInFuture { + ledger_time: current_time.as_nanos() as u64, + }); + } + + if current_time - created_at > Duration::from_secs(300) { + return Err(ApproveError::TooOld); + } + } + + Ok(()) + } + + pub fn inspect_icrc2_transfer_from(args: &TransferFromArgs) -> Result<(), TransferFromError> { + let default_fee: Nat = Configuration::get_fee().into(); + // check fee + if args + .fee + .as_ref() + .map(|fee| fee < &default_fee) + .unwrap_or(false) + { + return Err(TransferFromError::BadFee { + expected_fee: default_fee.into(), + }); + } + + // check if too old or in the future + if let Some(created_at) = args.created_at_time { + let current_time = Duration::from_nanos(time()); + let created_at = Duration::from_nanos(created_at); + + if created_at > current_time { + return Err(TransferFromError::CreatedInFuture { + ledger_time: current_time.as_nanos() as u64, + }); + } + + if current_time - created_at > Duration::from_secs(300) { + return Err(TransferFromError::TooOld); + } + } + + // check memo length + if let Some(memo) = &args.memo { + if memo.0.len() < 32 || memo.0.len() > 64 { + return Err(TransferFromError::GenericError { + error_code: Nat::from(0), + message: "Invalid memo length. I must have a length between 32 and 64 bytes" + .to_string(), + }); + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use icrc_ledger_types::icrc1::transfer::Memo; + + use super::*; + use crate::app::test_utils; + + #[test] + fn test_should_inspect_transfer() { + Configuration::set_fee(10_000); + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: Some((10_000 - 1).into()), + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_err()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: Some(10_000.into()), + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_ok()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_ok()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 31])), + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_err()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 65])), + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_err()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 32])), + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_ok()); + + let args = TransferArg { + from_subaccount: None, + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 64])), + created_at_time: None, + }; + + assert!(Inspect::inspect_transfer(&args).is_ok()); + } + + #[test] + fn test_should_inspect_icrc2_approve() { + Configuration::set_fee(10_000); + let caller = Principal::from_text("aaaaa-aa").unwrap(); + let args = ApproveArgs { + spender: test_utils::alice_account(), + amount: 100.into(), + fee: None, + expires_at: None, + created_at_time: None, + memo: None, + from_subaccount: None, + expected_allowance: None, + }; + + assert!(Inspect::inspect_icrc2_approve(caller, &args).is_ok()); + + let args = ApproveArgs { + spender: test_utils::alice_account(), + amount: 100.into(), + fee: Some((10_000 - 1).into()), + expires_at: None, + created_at_time: None, + memo: None, + from_subaccount: None, + expected_allowance: None, + }; + + assert!(Inspect::inspect_icrc2_approve(caller, &args).is_err()); + + let args = ApproveArgs { + spender: test_utils::alice_account(), + amount: 100.into(), + fee: None, + expires_at: None, + created_at_time: Some(0), + memo: None, + from_subaccount: None, + expected_allowance: None, + }; + + assert!(Inspect::inspect_icrc2_approve(caller, &args).is_err()); + + let args = ApproveArgs { + spender: test_utils::alice_account(), + amount: 100.into(), + fee: None, + expires_at: Some(0), + created_at_time: None, + memo: None, + from_subaccount: None, + expected_allowance: None, + }; + + assert!(Inspect::inspect_icrc2_approve(caller, &args).is_err()); + + let args = ApproveArgs { + spender: test_utils::alice_account(), + amount: 100.into(), + fee: None, + expires_at: None, + created_at_time: Some(crate::utils::time() * 2), + memo: None, + from_subaccount: None, + expected_allowance: None, + }; + + assert!(Inspect::inspect_icrc2_approve(caller, &args).is_err()); + } + + #[test] + fn test_should_inspect_transfer_from() { + Configuration::set_fee(10_000); + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: Some((10_000 - 1).into()), + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_err()); + + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: Some(10_000.into()), + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_ok()); + + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_ok()); + + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 31])), + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_err()); + + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 65])), + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_err()); + + let args = TransferFromArgs { + spender_subaccount: None, + from: test_utils::alice_account(), + to: test_utils::bob_account(), + amount: 100.into(), + fee: None, + memo: Some(Memo::from(vec![0; 32])), + created_at_time: None, + }; + + assert!(Inspect::inspect_icrc2_transfer_from(&args).is_ok()); + } +} diff --git a/src/app/memory.rs b/src/app/memory.rs new file mode 100644 index 0000000..643b280 --- /dev/null +++ b/src/app/memory.rs @@ -0,0 +1,46 @@ +use candid::{Decode, Encode}; +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager as IcMemoryManager}; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::{DefaultMemoryImpl, Storable}; +use icrc_ledger_types::icrc1::account::Account; + +pub const BALANCES_MEMORY_ID: MemoryId = MemoryId::new(10); +pub const CANISTER_WALLET_ACCOUNT_MEMORY_ID: MemoryId = MemoryId::new(12); +pub const SPEND_ALLOWANCE_MEMORY_ID: MemoryId = MemoryId::new(14); + +// Configuration +pub const MINTING_ACCOUNT_MEMORY_ID: MemoryId = MemoryId::new(20); +pub const NAME_MEMORY_ID: MemoryId = MemoryId::new(21); +pub const SYMBOL_MEMORY_ID: MemoryId = MemoryId::new(22); +pub const DECIMALS_MEMORY_ID: MemoryId = MemoryId::new(23); +pub const FEE_MEMORY_ID: MemoryId = MemoryId::new(24); +pub const LOGO_MEMORY_ID: MemoryId = MemoryId::new(25); + +thread_local! { + /// Memory manager + pub static MEMORY_MANAGER: IcMemoryManager = IcMemoryManager::init(DefaultMemoryImpl::default()); +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +pub struct StorableAccount(pub Account); + +impl From for StorableAccount { + fn from(value: Account) -> Self { + Self(value) + } +} + +impl Storable for StorableAccount { + const BOUND: Bound = Bound::Bounded { + max_size: 128, // principal + 32 bytes of subaccount + is_fixed_size: false, + }; + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + Decode!(&bytes, Account).unwrap().into() + } + + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + Encode!(&self.0).unwrap().into() + } +} diff --git a/src/app/spend_allowance.rs b/src/app/spend_allowance.rs new file mode 100644 index 0000000..4c991d3 --- /dev/null +++ b/src/app/spend_allowance.rs @@ -0,0 +1,538 @@ +mod key; +mod spend; + +use std::cell::RefCell; + +use candid::{Nat, Principal}; +use ic_stable_structures::memory_manager::VirtualMemory; +use ic_stable_structures::{DefaultMemoryImpl, StableBTreeMap}; +use icrc_ledger_types::icrc1::account::{Account, Subaccount}; +use icrc_ledger_types::icrc2::approve::ApproveArgs; +use key::AllowanceKey; +use spend::Spend; + +use crate::app::memory::{MEMORY_MANAGER, SPEND_ALLOWANCE_MEMORY_ID}; +use crate::app::{AllowanceError, CanisterError, CanisterResult}; + +thread_local! { + /// Spend allowance + static SPEND_ALLOWANCE: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(SPEND_ALLOWANCE_MEMORY_ID))) + ); + +} + +/// Takes care of verifying and storing spend allowance for ICRC2 token +pub struct SpendAllowance; + +impl SpendAllowance { + /// Approve a new spend from spender to caller. + /// + /// If the allowance already exists, then the amount is incremented. + pub fn approve_spend(caller: Principal, approve: ApproveArgs) -> CanisterResult { + // check if caller is the spender + if approve.spender.owner == caller { + return Err(CanisterError::Allowance(AllowanceError::BadSpender)); + } + // check if expiration is in the past + if approve + .expires_at + .map(|exp| exp < crate::utils::time()) + .unwrap_or_default() + { + return Err(CanisterError::Allowance(AllowanceError::BadExpiration)); + } + + let allowance_key = AllowanceKey::new( + Account { + owner: caller, + subaccount: approve.from_subaccount, + }, + approve.spender, + ); + let mut spend = Spend::from(approve); + + // if the allowance exists, then update current allowance + match Self::with_allowance_mut(&allowance_key, |existing_spend| { + // check expected allowance + if spend + .expected_allowance + .as_ref() + .map(|allowance| &existing_spend.amount != allowance) + .unwrap_or_default() + { + return Err(CanisterError::Allowance(AllowanceError::AllowanceChanged)); + } + + // increment spend amount and overwrite current speed + spend.amount += existing_spend.amount.clone(); + let new_amount = spend.amount.clone(); + *existing_spend = spend.clone(); + + Ok(new_amount) + }) { + Ok(new_amount) => return Ok(new_amount), + Err(CanisterError::Allowance(AllowanceError::AllowanceNotFound)) => {} + Err(err) => return Err(err), + }; + + let amount = spend.amount.clone(); + // check expected allowance to be None + if spend.expected_allowance.is_some() { + return Err(CanisterError::Allowance(AllowanceError::AllowanceChanged)); + } + + // if doesn't exist + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + allowances.insert(allowance_key, spend); + }); + + Ok(amount) + } + + /// Spend allowance from spender to caller. + pub fn spend_allowance( + caller: Principal, + from: Account, + amount: Nat, + spender_subaccount: Option, + ) -> CanisterResult<()> { + let spender = Account { + owner: caller, + subaccount: spender_subaccount, + }; + + let allowance_key = AllowanceKey::new(from, spender); + Self::with_allowance_mut(&allowance_key, |spend| { + // check if expired + if spend + .expires_at + .map(|exp| exp < crate::utils::time()) + .unwrap_or_default() + { + return Err(CanisterError::Allowance(AllowanceError::AllowanceExpired)); + } + // check balance + if spend.amount < amount { + return Err(CanisterError::Allowance(AllowanceError::InsufficientFunds)); + } + + spend.amount -= amount; + + Ok(()) + }) + } + + /// Get allowance for spender from owner. + pub fn get_allowance(owner: Account, spender: Account) -> (Nat, Option) { + let allowance_key = AllowanceKey::new(owner, spender); + Self::with_allowance(&allowance_key, |spend| { + (spend.amount.clone(), spend.expires_at) + }) + .unwrap_or_default() + } + + /// Remove the expired allowance from the map + #[allow(unused)] + pub fn remove_expired_allowance() { + let now = crate::utils::time(); + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + let mut expired_allowances = vec![]; + for (key, spend) in allowances.iter() { + if spend.expires_at.map(|exp| exp < now).unwrap_or_default() || spend.amount == 0 { + expired_allowances.push(key.clone()); + } + } + + for key in expired_allowances { + allowances.remove(&key); + } + }); + } + + fn with_allowance(allowance: &AllowanceKey, f: F) -> CanisterResult + where + F: FnOnce(&Spend) -> T, + { + SPEND_ALLOWANCE.with_borrow(|allowances| match allowances.get(allowance) { + Some(balance) => Ok(f(&balance)), + None => Err(CanisterError::Allowance(AllowanceError::AllowanceNotFound)), + }) + } + + fn with_allowance_mut(allowance: &AllowanceKey, f: F) -> CanisterResult + where + F: FnOnce(&mut Spend) -> CanisterResult, + { + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + let mut spend = allowances + .get(allowance) + .ok_or(CanisterError::Allowance(AllowanceError::AllowanceNotFound))?; + let res = f(&mut spend)?; + + allowances.insert(allowance.clone(), spend); + + Ok(res) + }) + } +} + +#[cfg(test)] +mod test { + + use std::time::Duration; + + use candid::Principal; + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::{alice_account, bob_account, caller_account}; + use crate::utils::caller; + + #[test] + fn test_should_insert_new_allowance() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + let allowance_key = AllowanceKey::new(caller_account(), allowance.spender); + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + let spend = SPEND_ALLOWANCE.with_borrow(|allowances| allowances.get(&allowance_key)); + + assert!(spend.is_some()); + assert_eq!(spend.unwrap().amount, allowance.amount); + } + + #[test] + fn test_should_overwrite_allowance() { + let exp_1 = crate::utils::time() * 2; + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(exp_1), + fee: None, + memo: None, + created_at_time: None, + }; + + let allowance_key = AllowanceKey::new(caller_account(), allowance.spender); + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + // overwrite + + let exp_2 = crate::utils::time() * 3; + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 150.into(), + expected_allowance: None, + expires_at: Some(exp_2), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + let spend = SPEND_ALLOWANCE.with_borrow(|allowances| allowances.get(&allowance_key)); + + assert!(spend.is_some()); + assert_eq!(spend.as_ref().unwrap().amount, 250_u64); + assert_eq!(spend.as_ref().unwrap().expires_at.unwrap(), exp_2); + } + + #[test] + fn test_should_not_authorize_same_spender() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: crate::app::test_utils::caller_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_err()); + } + + #[test] + fn test_should_not_authorize_expired_allowance() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(crate::utils::time() - 1), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_err()); + } + + #[test] + fn test_should_not_authorize_changed_allowance() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: Some(50.into()), + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_err()); + + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: Some(100.into()), + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + } + + #[test] + fn test_should_not_authorize_changed_allowance_on_new_allowance() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: Some(50.into()), + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_err()); + } + + #[test] + fn test_should_spend_allowance() { + let allowance = ApproveArgs { + from_subaccount: bob_account().subaccount, + spender: caller_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + let allowance_key = AllowanceKey::new(bob_account(), caller_account()); + let spend = Spend::from(allowance); + + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + allowances.insert(allowance_key.clone(), spend); + }); + + assert!(SpendAllowance::spend_allowance( + caller(), + bob_account(), + 25.into(), + caller_account().subaccount + ) + .is_ok()); + let spend = SPEND_ALLOWANCE.with_borrow(|allowances| allowances.get(&allowance_key)); + + assert!(spend.is_some()); + assert_eq!(spend.as_ref().unwrap().amount, 75_u64); + } + + #[test] + fn test_should_not_spend_allowance_if_expired() { + let allowance = ApproveArgs { + from_subaccount: bob_account().subaccount, + spender: caller_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(crate::utils::time() + 100_000), + fee: None, + memo: None, + created_at_time: None, + }; + + let allowance_key = AllowanceKey::new(bob_account(), caller_account()); + let spend = Spend::from(allowance); + + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + allowances.insert(allowance_key.clone(), spend); + }); + + std::thread::sleep(Duration::from_millis(100)); + + assert!(SpendAllowance::spend_allowance( + caller(), + bob_account(), + 25.into(), + caller_account().subaccount + ) + .is_err()); + } + + #[test] + fn test_should_not_spend_allowance_if_insufficient_funds() { + let allowance = ApproveArgs { + from_subaccount: bob_account().subaccount, + spender: caller_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + let allowance_key = AllowanceKey::new(bob_account(), caller_account()); + let spend = Spend::from(allowance); + + SPEND_ALLOWANCE.with_borrow_mut(|allowances| { + allowances.insert(allowance_key, spend); + }); + + assert!(SpendAllowance::spend_allowance( + caller(), + bob_account(), + 125.into(), + caller_account().subaccount + ) + .is_err()); + } + + #[test] + fn test_should_get_allowance() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + assert_eq!( + SpendAllowance::get_allowance(caller_account(), alice_account()), + (100.into(), None) + ); + + let exp = crate::utils::time() * 2; + + let allowance = ApproveArgs { + from_subaccount: None, + spender: bob_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(exp), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + assert_eq!( + SpendAllowance::get_allowance(caller_account(), bob_account()), + (100.into(), Some(exp)) + ); + + // unexisting account + assert_eq!( + SpendAllowance::get_allowance(alice_account(), bob_account()), + (0.into(), None) + ); + } + + #[test] + fn test_should_remove_expired_allowances() { + let allowance = ApproveArgs { + from_subaccount: None, + spender: alice_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(crate::utils::time() * 2), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + let allowance = ApproveArgs { + from_subaccount: None, + spender: bob_account(), + amount: 100.into(), + expected_allowance: None, + expires_at: Some(crate::utils::time() + 100_000), + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + let allowance = ApproveArgs { + from_subaccount: None, + spender: Account { + owner: Principal::management_canister(), + subaccount: None, + }, + amount: 0.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: None, + }; + + assert!(SpendAllowance::approve_spend(caller(), allowance.clone()).is_ok()); + + std::thread::sleep(Duration::from_millis(100)); + + SpendAllowance::remove_expired_allowance(); + + assert_eq!( + SPEND_ALLOWANCE.with_borrow(|allowances| allowances.len()), + 1 + ); + } +} diff --git a/src/app/spend_allowance/key.rs b/src/app/spend_allowance/key.rs new file mode 100644 index 0000000..651be3a --- /dev/null +++ b/src/app/spend_allowance/key.rs @@ -0,0 +1,96 @@ +use candid::{CandidType, Decode, Deserialize, Encode}; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::Storable; +use icrc_ledger_types::icrc1::account::Account; + +use crate::app::memory::StorableAccount; + +#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +/// Allowance key for mapping (from, spender) to allowance +pub struct AllowanceKey { + pub balance_owner: StorableAccount, + pub spender: StorableAccount, +} + +impl AllowanceKey { + pub fn new(balance_owner: Account, spender: Account) -> Self { + Self { + balance_owner: balance_owner.into(), + spender: spender.into(), + } + } +} + +impl From for AllowanceKey { + fn from(value: Codec) -> Self { + Self { + balance_owner: value.from.into(), + spender: value.spender.into(), + } + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord, CandidType, Deserialize)] +struct Codec { + from: Account, + spender: Account, +} + +impl Storable for AllowanceKey { + const BOUND: Bound = Bound::Bounded { + max_size: StorableAccount::BOUND.max_size() * 2, + is_fixed_size: false, + }; + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + Decode!(&bytes, Codec).unwrap().into() + } + + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + let codec = Codec { + from: self.balance_owner.clone().0, + spender: self.spender.clone().0, + }; + Encode!(&codec).unwrap().into() + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::{alice_account, bob_account}; + use crate::utils::caller; + + #[test] + fn test_should_encode_and_decode_allowance_key() { + let allowance_key = AllowanceKey { + balance_owner: bob_account().into(), + spender: alice_account().into(), + }; + + let encoded = allowance_key.to_bytes(); + let decoded = AllowanceKey::from_bytes(encoded); + + assert_eq!(allowance_key, decoded); + } + + #[test] + fn test_should_encode_and_decode_allowance_key_with_none() { + let allowance_key = AllowanceKey { + balance_owner: bob_account().into(), + spender: Account { + owner: caller(), + subaccount: None, + } + .into(), + }; + + let encoded = allowance_key.to_bytes(); + let decoded = AllowanceKey::from_bytes(encoded); + + assert_eq!(allowance_key, decoded); + } +} diff --git a/src/app/spend_allowance/spend.rs b/src/app/spend_allowance/spend.rs new file mode 100644 index 0000000..f7c3ea5 --- /dev/null +++ b/src/app/spend_allowance/spend.rs @@ -0,0 +1,182 @@ +use bytes::{Buf as _, BufMut as _, Bytes, BytesMut}; +use candid::Nat; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::Storable; +use icrc_ledger_types::icrc1::transfer::Memo; +use icrc_ledger_types::icrc2::approve::ApproveArgs; +use num_bigint::BigUint; + +/// Storable spend allowance type +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Spend { + /// spenable amount + pub amount: Nat, + /// If the expected_allowance field is set, it's equal to the current allowance for the spender. + pub expected_allowance: Option, + pub expires_at: Option, + pub fee: Option, + pub memo: Option, + pub created_at_time: u64, +} + +impl From for Spend { + fn from(value: ApproveArgs) -> Self { + Self { + amount: value.amount, + expected_allowance: value.expected_allowance, + expires_at: value.expires_at, + fee: value.fee, + memo: value.memo, + created_at_time: value.created_at_time.unwrap_or_else(crate::utils::time), + } + } +} + +impl Storable for Spend { + const BOUND: Bound = Bound::Bounded { + max_size: 256, + is_fixed_size: false, + }; + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + let mut bytes = Bytes::from(bytes.to_vec()); + let amount_len = bytes.get_u8() as usize; + let amount = BigUint::from_bytes_be(&bytes.slice(..amount_len)).into(); + bytes.advance(amount_len); + + let expected_allowance_len = bytes.get_u8() as usize; + let expected_allowance = if expected_allowance_len > 0 { + let expected_allowance = + BigUint::from_bytes_be(&bytes.slice(..expected_allowance_len)).into(); + bytes.advance(expected_allowance_len); + + Some(expected_allowance) + } else { + None + }; + + let expires_at = if bytes.get_u8() == 1 { + let expires_at = bytes.get_u64(); + + Some(expires_at) + } else { + None + }; + + let fee_len = bytes.get_u8() as usize; + let fee = if fee_len > 0 { + let fee = BigUint::from_bytes_be(&bytes.slice(..fee_len)).into(); + bytes.advance(fee_len); + + Some(fee) + } else { + None + }; + + let memo_len = bytes.get_u8() as usize; + let memo = if memo_len > 0 { + let memo = Memo::from(bytes.slice(..memo_len).to_vec()); + bytes.advance(memo_len); + + Some(memo) + } else { + None + }; + + let created_at_time = bytes.get_u64(); + + Self { + amount, + expected_allowance, + expires_at, + fee, + memo, + created_at_time, + } + } + + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + let mut buffer = BytesMut::with_capacity(Self::BOUND.max_size() as usize); + + // amount + let amount_bytes = self.amount.0.to_bytes_be(); + buffer.put_u8(amount_bytes.len() as u8); + buffer.put(self.amount.0.to_bytes_be().as_slice()); + + // expected allowance + let expected_allowance = self + .expected_allowance + .as_ref() + .map(|x| x.0.to_bytes_be()) + .unwrap_or_default(); + buffer.put_u8(expected_allowance.len() as u8); // if zero, don't read expected allowance + buffer.put(expected_allowance.as_slice()); + + // expires at + buffer.put_u8(self.expires_at.is_some() as u8); // 1 if expires_at is some + if let Some(expires_at) = self.expires_at { + buffer.put_u64(expires_at); + } + + // fee + let fee = self + .fee + .as_ref() + .map(|x| x.0.to_bytes_be()) + .unwrap_or_default(); + buffer.put_u8(fee.len() as u8); // if zero, don't read expected allowance + buffer.put(fee.as_slice()); + + // memo + let memo = self.memo.as_ref().map(|x| x.0.to_vec()).unwrap_or_default(); + buffer.put_u8(memo.len() as u8); // if zero, don't read expected allowance + buffer.put(memo.as_slice()); + + // created_at_time + buffer.put_u64(self.created_at_time); + + buffer.to_vec().into() + } +} + +#[cfg(test)] +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_encode_and_decode_with_options_none() { + let spend = Spend { + amount: 100_000_000_000_000_u64.into(), + expected_allowance: None, + expires_at: None, + fee: None, + memo: None, + created_at_time: crate::utils::time(), + }; + + let encoded = spend.to_bytes(); + let decoded = Spend::from_bytes(encoded); + + assert_eq!(spend, decoded); + } + + #[test] + fn test_should_encode_and_decode_with_options_some() { + let spend = Spend { + amount: 100_000_000_000_000_u64.into(), + expected_allowance: Some(100_000_000_000_000_u64.into()), + expires_at: Some(crate::utils::time()), + fee: Some(100_000_000_000_000_u64.into()), + memo: Some(Memo::from(vec![1; 48])), + created_at_time: crate::utils::time(), + }; + + let encoded = spend.to_bytes(); + let decoded = Spend::from_bytes(encoded); + + assert_eq!(spend, decoded); + } +} diff --git a/src/app/test_utils.rs b/src/app/test_utils.rs new file mode 100644 index 0000000..f3d3ac1 --- /dev/null +++ b/src/app/test_utils.rs @@ -0,0 +1,69 @@ +use candid::{Nat, Principal}; +use icrc_ledger_types::icrc1::account::{Account, DEFAULT_SUBACCOUNT}; + +use crate::utils::caller; + +pub fn alice() -> Principal { + Principal::from_text("be2us-64aaa-aaaaa-qaabq-cai").unwrap() +} + +pub fn alice_account() -> Account { + Account { + owner: alice(), + subaccount: Some(*DEFAULT_SUBACCOUNT), + } +} + +pub fn bob() -> Principal { + Principal::from_text("bs5l3-6b3zu-dpqyj-p2x4a-jyg4k-goneb-afof2-y5d62-skt67-3756q-dqe").unwrap() +} + +pub fn bob_account() -> Account { + Account { + owner: bob(), + subaccount: Some([ + 0x21, 0xa9, 0x95, 0x49, 0xe7, 0x92, 0x90, 0x7c, 0x5e, 0x27, 0x5e, 0x54, 0x51, 0x06, + 0x8d, 0x4d, 0xdf, 0x4d, 0x43, 0xee, 0x8d, 0xca, 0xb4, 0x87, 0x56, 0x23, 0x1a, 0x8f, + 0xb7, 0x71, 0x31, 0x23, + ]), + } +} + +pub fn minting_account() -> Account { + Account { + owner: crate::utils::id(), + subaccount: Some([ + 0x21, 0xa9, 0x95, 0x49, 0xe7, 0x92, 0x90, 0x7c, 0x5e, 0x27, 0x5e, 0x54, 0x51, 0x06, + 0x8d, 0xad, 0xdf, 0x4d, 0x43, 0xee, 0x8d, 0xca, 0xb4, 0x87, 0x56, 0x23, 0x1a, 0x8f, + 0xb7, 0x71, 0x31, 0x23, + ]), + } +} + +pub fn caller_account() -> Account { + Account { + owner: caller(), + subaccount: Some(*DEFAULT_SUBACCOUNT), + } +} + +/// Convert fly to picofly +pub fn int_to_decimals(amount: u64) -> Nat { + let amount = Nat::from(amount); + let multiplier = Nat::from(1_000_000_000_000_u64); + amount * multiplier +} + +mod test { + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_convert_fly_to_picofly() { + assert_eq!(int_to_decimals(1), 1_000_000_000_000_u64); + assert_eq!(int_to_decimals(20), 20_000_000_000_000_u64); + assert_eq!(int_to_decimals(300), 300_000_000_000_000_u64); + } +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..6988a9c --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,18 @@ +use std::time::Duration; + +/// The ledger will refuse transactions older than this or newer than this +pub const ICRC1_TX_TIME_SKID: Duration = Duration::from_secs(60 * 5); + +/// The ledger canister id of the ICP token +#[cfg(target_arch = "wasm32")] +pub const ICP_LEDGER_CANISTER: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; +/// The ledger canister id of the CKBTC token +#[cfg(target_arch = "wasm32")] +pub const CKBTC_LEDGER_CANISTER: &str = "mxzaz-hqaaa-aaaar-qaada-cai"; + +#[cfg(target_family = "wasm")] +pub const SPEND_ALLOWANCE_EXPIRED_ALLOWANCE_TIMER_INTERVAL: Duration = + Duration::from_secs(60 * 60 * 24 * 7); // 7 days + +#[cfg(target_family = "wasm")] +pub const LIQUIDITY_POOL_SWAP_INTERVAL: Duration = Duration::from_secs(60 * 60 * 24); // 1 day diff --git a/src/icrc2-template-canister.did b/src/icrc2-template-canister.did new file mode 100644 index 0000000..c20fe65 --- /dev/null +++ b/src/icrc2-template-canister.did @@ -0,0 +1,97 @@ +type Account = record { owner : principal; subaccount : opt vec nat8 }; +type Allowance = record { allowance : nat; expires_at : opt nat64 }; +type AllowanceArgs = record { account : Account; spender : Account }; +type ApproveArgs = record { + fee : opt nat; + memo : opt vec nat8; + from_subaccount : opt vec nat8; + created_at_time : opt nat64; + amount : nat; + expected_allowance : opt nat; + expires_at : opt nat64; + spender : Account; +}; +type ApproveError = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + Duplicate : record { duplicate_of : nat }; + BadFee : record { expected_fee : nat }; + AllowanceChanged : record { current_allowance : nat }; + CreatedInFuture : record { ledger_time : nat64 }; + TooOld; + Expired : record { ledger_time : nat64 }; + InsufficientFunds : record { balance : nat }; +}; +type InitArgs = record { + fee : nat64; + decimals : nat8; + minting_account : Account; + logo : text; + name : text; + accounts : vec record { Account; nat }; + total_supply : nat; + symbol : text; +}; +type MetadataValue = variant { + Int : int; + Nat : nat; + Blob : vec nat8; + Text : text; +}; +type Result = variant { Ok : nat; Err : TransferError }; +type Result_1 = variant { Ok : nat; Err : ApproveError }; +type Result_2 = variant { Ok : nat; Err : TransferFromError }; +type TokenExtension = record { url : text; name : text }; +type TransferArg = record { + to : Account; + fee : opt nat; + memo : opt vec nat8; + from_subaccount : opt vec nat8; + created_at_time : opt nat64; + amount : nat; +}; +type TransferError = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + BadBurn : record { min_burn_amount : nat }; + Duplicate : record { duplicate_of : nat }; + BadFee : record { expected_fee : nat }; + CreatedInFuture : record { ledger_time : nat64 }; + TooOld; + InsufficientFunds : record { balance : nat }; +}; +type TransferFromArgs = record { + to : Account; + fee : opt nat; + spender_subaccount : opt vec nat8; + from : Account; + memo : opt vec nat8; + created_at_time : opt nat64; + amount : nat; +}; +type TransferFromError = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + InsufficientAllowance : record { allowance : nat }; + BadBurn : record { min_burn_amount : nat }; + Duplicate : record { duplicate_of : nat }; + BadFee : record { expected_fee : nat }; + CreatedInFuture : record { ledger_time : nat64 }; + TooOld; + InsufficientFunds : record { balance : nat }; +}; +service : (InitArgs) -> { + cycles : () -> (nat) query; + icrc1_balance_of : (Account) -> (nat) query; + icrc1_decimals : () -> (nat8) query; + icrc1_fee : () -> (nat) query; + icrc1_metadata : () -> (vec record { text; MetadataValue }) query; + icrc1_name : () -> (text) query; + icrc1_supported_standards : () -> (vec TokenExtension) query; + icrc1_symbol : () -> (text) query; + icrc1_total_supply : () -> (nat) query; + icrc1_transfer : (TransferArg) -> (Result); + icrc2_allowance : (AllowanceArgs) -> (Allowance) query; + icrc2_approve : (ApproveArgs) -> (Result_1); + icrc2_transfer_from : (TransferFromArgs) -> (Result_2); +} \ No newline at end of file diff --git a/src/inspect.rs b/src/inspect.rs new file mode 100644 index 0000000..05a4d34 --- /dev/null +++ b/src/inspect.rs @@ -0,0 +1,47 @@ +use ic_cdk::api; +#[cfg(target_family = "wasm")] +use ic_cdk_macros::inspect_message; +use icrc_ledger_types::icrc1::transfer::TransferArg; + +use crate::app::Inspect; +use crate::utils::caller; + +/// NOTE: inspect is disabled for non-wasm targets because without it we are getting a weird compilation error +/// in CI: +/// > multiple definition of `canister_inspect_message' +#[cfg(target_family = "wasm")] +#[inspect_message] +fn inspect_messages() { + inspect_message_impl() +} + +#[allow(dead_code)] +fn inspect_message_impl() { + let method = api::call::method_name(); + + let check_result = match method.as_str() { + "icrc1_transfer" => { + let transfer_arg = api::call::arg_data::<(TransferArg,)>().0; + Inspect::inspect_transfer(&transfer_arg).is_ok() + } + "icrc2_approve" => { + let args = api::call::arg_data::<(icrc_ledger_types::icrc2::approve::ApproveArgs,)>().0; + Inspect::inspect_icrc2_approve(caller(), &args).is_ok() + } + "icrc2_transfer_from" => { + let args = api::call::arg_data::<( + icrc_ledger_types::icrc2::transfer_from::TransferFromArgs, + )>() + .0; + Inspect::inspect_icrc2_transfer_from(&args).is_ok() + } + + _ => true, + }; + + if check_result { + api::call::accept_message(); + } else { + ic_cdk::trap("Bad request"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9dbc946 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,160 @@ +//! # Fly +//! +//! The fly canister serves a ICRC-2 token called $FLY, which is the reward token for Deferred transactions. +//! It is a deflationary token which ... + +mod app; +mod constants; +mod inspect; +mod utils; + +use candid::{candid_method, CandidType, Nat}; +use ic_cdk_macros::{init, post_upgrade, query, update}; +use icrc_ledger_types::icrc::generic_metadata_value::MetadataValue; +use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::transfer as icrc1_transfer; +use icrc_ledger_types::icrc1::transfer::TransferArg; +use serde::Deserialize; + +use self::app::Icrc2Canister; + +#[derive(CandidType, Clone, Debug)] +pub struct TokenExtension { + pub name: String, + pub url: String, +} + +impl TokenExtension { + /// Returns extension for icrc-1 + pub fn icrc1() -> Self { + Self { + name: "ICRC-1".to_string(), + url: "https://github.com/dfinity/ICRC-1".to_string(), + } + } + + /// Returns extension for icrc-2 + pub fn icrc2() -> Self { + Self { + name: "ICRC-2".to_string(), + url: "https://github.com/dfinity/ICRC-1".to_string(), + } + } +} + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct InitArgs { + pub accounts: Vec<(Account, Nat)>, + pub decimals: u8, + pub fee: u64, + pub logo: String, + pub minting_account: Account, + pub name: String, + pub symbol: String, + pub total_supply: Nat, +} + +#[init] +pub fn init(data: InitArgs) { + Icrc2Canister::init(data); +} + +#[post_upgrade] +pub fn post_upgrade() { + Icrc2Canister::post_upgrade(); +} + +#[query] +#[candid_method(query)] +pub fn cycles() -> Nat { + Icrc2Canister::cycles() +} + +// icrc-1 + +#[query] +#[candid_method(query)] +pub fn icrc1_name() -> String { + Icrc2Canister::icrc1_name() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_symbol() -> String { + Icrc2Canister::icrc1_symbol() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_decimals() -> u8 { + Icrc2Canister::icrc1_decimals() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_fee() -> Nat { + Icrc2Canister::icrc1_fee() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_metadata() -> Vec<(String, MetadataValue)> { + Icrc2Canister::icrc1_metadata() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_total_supply() -> Nat { + Icrc2Canister::icrc1_total_supply() +} + +#[query] +#[candid_method(query)] +pub fn icrc1_balance_of(account: Account) -> Nat { + Icrc2Canister::icrc1_balance_of(account) +} + +#[update] +#[candid_method(update)] +pub fn icrc1_transfer(transfer_args: TransferArg) -> Result { + Icrc2Canister::icrc1_transfer(transfer_args) +} + +#[query] +#[candid_method(query)] +pub fn icrc1_supported_standards() -> Vec { + Icrc2Canister::icrc1_supported_standards() +} + +#[update] +#[candid_method(update)] +pub fn icrc2_approve( + args: icrc_ledger_types::icrc2::approve::ApproveArgs, +) -> Result { + Icrc2Canister::icrc2_approve(args) +} + +#[update] +#[candid_method(update)] +pub fn icrc2_transfer_from( + args: icrc_ledger_types::icrc2::transfer_from::TransferFromArgs, +) -> Result { + Icrc2Canister::icrc2_transfer_from(args) +} + +#[query] +#[candid_method(query)] +pub fn icrc2_allowance( + args: icrc_ledger_types::icrc2::allowance::AllowanceArgs, +) -> icrc_ledger_types::icrc2::allowance::Allowance { + Icrc2Canister::icrc2_allowance(args) +} + +#[allow(dead_code)] +fn main() { + // The line below generates did types and service definition from the + // methods annotated with `candid_method` above. The definition is then + // obtained with `__export_service()`. + candid::export_service!(); + std::print!("{}", __export_service()); +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..4938a00 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,71 @@ +use candid::{Nat, Principal}; + +/// Returns current time in nanoseconds +pub fn time() -> u64 { + #[cfg(not(target_arch = "wasm32"))] + { + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap(); + time.as_nanos() as u64 + } + #[cfg(target_arch = "wasm32")] + { + ic_cdk::api::time() + } +} + +/// Returns canister id +pub fn id() -> Principal { + #[cfg(not(target_arch = "wasm32"))] + { + Principal::from_text("lj532-6iaaa-aaaah-qcc7a-cai").unwrap() + } + #[cfg(target_arch = "wasm32")] + { + ic_cdk::api::id() + } +} + +pub fn cycles() -> Nat { + #[cfg(not(target_arch = "wasm32"))] + { + Nat::from(30_000_000_000_u64) + } + #[cfg(target_arch = "wasm32")] + { + ic_cdk::api::canister_balance().into() + } +} + +pub fn caller() -> Principal { + #[cfg(not(target_arch = "wasm32"))] + { + Principal::from_text("zrrb4-gyxmq-nx67d-wmbky-k6xyt-byhmw-tr5ct-vsxu4-nuv2g-6rr65-aae") + .unwrap() + } + #[cfg(target_arch = "wasm32")] + { + ic_cdk::caller() + } +} + +/// Generates a random subaccount +#[cfg(test)] +pub async fn random_subaccount() -> icrc_ledger_types::icrc1::account::Subaccount { + #[cfg(test)] + { + let random_bytes = rand::random::<[u8; 32]>(); + icrc_ledger_types::icrc1::account::Subaccount::from(random_bytes) + } + #[cfg(not(test))] + { + let random_bytes = ic_cdk::api::management_canister::main::raw_rand() + .await + .unwrap() + .0; + + let random_bytes: [u8; 32] = random_bytes.try_into().unwrap(); + icrc_ledger_types::icrc1::account::Subaccount::from(random_bytes) + } +}