diff --git a/.env.example b/.env.example index 3f771a2..031d522 100644 --- a/.env.example +++ b/.env.example @@ -1,35 +1,35 @@ -# DFX CANISTER ENVIRONMENT VARIABLES -DFX_VERSION= -DFX_NETWORK= -CANISTER_CANDID_PATH_SHARED= -CANISTER_ID_USER= -CANISTER_ID_TOWNTALK= -CANISTER_ID_STORAGE= -CANISTER_ID_SHARED= -CANISTER_ID_INTERNET_IDENTITY= -CANISTER_ID_ICRC1_LEDGER_CANISTER= -CANISTER_ID_ICP_LEDGER_CANISTER= -CANISTER_ID_GRINDARENA= -CANISTER_ID_FRONTEND= -CANISTER_ID_COURSE= -CANISTER_ID= -CANISTER_CANDID_PATH= -# END DFX CANISTER ENVIRONMENT VARIABLES - -# DFX TOKEN VARIABLES -TOKEN_NAME= -TOKEN_SYMBOL= -PRE_MINTED_TOKENS= -TRANSFER_FEE= -TRIGGER_THRESHOLD= -NUM_OF_BLOCK_TO_ARCHIVE= -CYCLE_FOR_ARCHIVE_CREATION= -FEATURE_FLAGS= - -# DEFAULT PRINCIPALS / IDS FOR TOKEN DEPLOYMENT ACCOUNTS -MINTER_ACCOUNT_ID= -DEPLOY_ID= -ARCHIVE_CONTROLLER= - -# Local development args +# DFX CANISTER ENVIRONMENT VARIABLES +DFX_VERSION= +DFX_NETWORK= +CANISTER_CANDID_PATH_SHARED= +CANISTER_ID_USER= +CANISTER_ID_TOWNTALK= +CANISTER_ID_STORAGE= +CANISTER_ID_SHARED= +CANISTER_ID_INTERNET_IDENTITY= +CANISTER_ID_ICRC1_LEDGER_CANISTER= +CANISTER_ID_ICP_LEDGER_CANISTER= +CANISTER_ID_GRINDARENA= +CANISTER_ID_FRONTEND= +CANISTER_ID_COURSE= +CANISTER_ID= +CANISTER_CANDID_PATH= +# END DFX CANISTER ENVIRONMENT VARIABLES + +# DFX TOKEN VARIABLES +TOKEN_NAME= +TOKEN_SYMBOL= +PRE_MINTED_TOKENS= +TRANSFER_FEE= +TRIGGER_THRESHOLD= +NUM_OF_BLOCK_TO_ARCHIVE= +CYCLE_FOR_ARCHIVE_CREATION= +FEATURE_FLAGS= + +# DEFAULT PRINCIPALS / IDS FOR TOKEN DEPLOYMENT ACCOUNTS +MINTER_ACCOUNT_ID= +DEPLOY_ID= +ARCHIVE_CONTROLLER= + +# Local development args IS_LOCAL_DEV= \ No newline at end of file diff --git a/.gitignore b/.gitignore index d2fd733..0187e7d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,26 +1,29 @@ -# Various IDEs and Editors -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env - -venv* \ No newline at end of file +# Various IDEs and Editors +.idea/ +**/*~ + +# Mac OSX temporary files +.DS_Store +**/.DS_Store + +# dfx temporary files +.dfx/ + +# generated files +**/declarations/ + +# rust +target/ + +# frontend code +node_modules/ +dist/ +.svelte-kit/ + +# environment variables +.env + +venv* + +nns_install.log +bishop.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 17bdd9b..6b3885c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,1047 +1,1047 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[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 = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "candid" -version = "0.10.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d90f5a1426d0489283a0bd5da9ed406fb3e69597e0d823dcb88a1965bb58d2" -dependencies = [ - "anyhow", - "binread", - "byteorder", - "candid_derive", - "hex", - "ic_principal", - "leb128", - "num-bigint", - "num-traits", - "paste", - "pretty", - "serde", - "serde_bytes", - "stacker", - "thiserror 1.0.69", -] - -[[package]] -name = "candid_derive" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" -dependencies = [ - "lazy_static", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "cc" -version = "1.2.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "course" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "serde", - "user", - "utilities", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[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 = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[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 = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[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.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -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 = "grindarena" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "serde", - "utilities", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[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.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9cc3e0e86ee12504c749fa33793014f1f4d6956a8a70e4db595169c5f6ac26" -dependencies = [ - "candid", - "ic-cdk-executor", - "ic-cdk-macros", - "ic-error-types", - "ic-management-canister-types", - "ic0", - "serde", - "serde_bytes", - "slotmap", - "thiserror 2.0.12", -] - -[[package]] -name = "ic-cdk-executor" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15948808e3e7b50749fe50838df77fccaf048c8af2c26884ff5c8f787c29787a" -dependencies = [ - "slotmap", -] - -[[package]] -name = "ic-cdk-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b190cace2b141a5801252115bdc27397d47f086c928af3e917ce1da81b17e3cd" -dependencies = [ - "candid", - "darling", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "ic-cdk-timers" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea87cf31444de833db85bbd15e97bc135ee14529b13158ffdaf6530bf6d7e85" -dependencies = [ - "candid", - "futures", - "ic-cdk", - "ic0", - "serde", - "serde_bytes", - "slotmap", -] - -[[package]] -name = "ic-error-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" -dependencies = [ - "serde", - "strum", - "strum_macros", -] - -[[package]] -name = "ic-management-canister-types" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98554c2d8a30c00b6bfda18062fdcef21215cad07a52d8b8b1eb3130e51bfe71" -dependencies = [ - "candid", - "serde", - "serde_bytes", -] - -[[package]] -name = "ic0" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" - -[[package]] -name = "ic_principal" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" -dependencies = [ - "crc32fast", - "data-encoding", - "serde", - "sha2", - "thiserror 1.0.69", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[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.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "memchr" -version = "2.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "paginator" -version = "0.1.0" -dependencies = [ - "candid", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "pretty" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" -dependencies = [ - "arrayvec", - "typed-arena", - "unicode-width", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "psm" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" -dependencies = [ - "cc", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shared" -version = "0.1.0" -dependencies = [ - "candid", - "hex", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "paginator", - "serde", - "sha2", - "uuid", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" - -[[package]] -name = "slotmap" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" -dependencies = [ - "version_check", -] - -[[package]] -name = "stacker" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys", -] - -[[package]] -name = "storage" -version = "0.1.0" -dependencies = [ - "candid", - "hex", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "paginator", - "serde", - "sha2", - "utilities", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", -] - -[[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.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "towntalk" -version = "0.1.0" -dependencies = [ - "candid", - "futures", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "paginator", - "serde", - "utilities", -] - -[[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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "user" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "serde", -] - -[[package]] -name = "utilities" -version = "0.1.0" -dependencies = [ - "candid", - "ic-cdk", - "ic-cdk-timers", - "ic_principal", - "serde", - "time", - "uuid", -] - -[[package]] -name = "uuid" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[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 = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "candid" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d90f5a1426d0489283a0bd5da9ed406fb3e69597e0d823dcb88a1965bb58d2" +dependencies = [ + "anyhow", + "binread", + "byteorder", + "candid_derive", + "hex", + "ic_principal", + "leb128", + "num-bigint", + "num-traits", + "paste", + "pretty", + "serde", + "serde_bytes", + "stacker", + "thiserror 1.0.69", +] + +[[package]] +name = "candid_derive" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" +dependencies = [ + "lazy_static", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "course" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "serde", + "user", + "utilities", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[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 = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[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 = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +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 = "grindarena" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "serde", + "utilities", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[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.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9cc3e0e86ee12504c749fa33793014f1f4d6956a8a70e4db595169c5f6ac26" +dependencies = [ + "candid", + "ic-cdk-executor", + "ic-cdk-macros", + "ic-error-types", + "ic-management-canister-types", + "ic0", + "serde", + "serde_bytes", + "slotmap", + "thiserror 2.0.12", +] + +[[package]] +name = "ic-cdk-executor" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15948808e3e7b50749fe50838df77fccaf048c8af2c26884ff5c8f787c29787a" +dependencies = [ + "slotmap", +] + +[[package]] +name = "ic-cdk-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b190cace2b141a5801252115bdc27397d47f086c928af3e917ce1da81b17e3cd" +dependencies = [ + "candid", + "darling", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "ic-cdk-timers" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea87cf31444de833db85bbd15e97bc135ee14529b13158ffdaf6530bf6d7e85" +dependencies = [ + "candid", + "futures", + "ic-cdk", + "ic0", + "serde", + "serde_bytes", + "slotmap", +] + +[[package]] +name = "ic-error-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbeeb3d91aa179d6496d7293becdacedfc413c825cac79fd54ea1906f003ee55" +dependencies = [ + "serde", + "strum", + "strum_macros", +] + +[[package]] +name = "ic-management-canister-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98554c2d8a30c00b6bfda18062fdcef21215cad07a52d8b8b1eb3130e51bfe71" +dependencies = [ + "candid", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic0" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8877193e1921b5fd16accb0305eb46016868cd1935b05c05eca0ec007b943272" + +[[package]] +name = "ic_principal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +dependencies = [ + "crc32fast", + "data-encoding", + "serde", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[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.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "paginator" +version = "0.1.0" +dependencies = [ + "candid", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "pretty" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac98773b7109bc75f475ab5a134c9b64b87e59d776d31098d8f346922396a477" +dependencies = [ + "arrayvec", + "typed-arena", + "unicode-width", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +dependencies = [ + "cc", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared" +version = "0.1.0" +dependencies = [ + "candid", + "hex", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "paginator", + "serde", + "sha2", + "uuid", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + +[[package]] +name = "storage" +version = "0.1.0" +dependencies = [ + "candid", + "hex", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "paginator", + "serde", + "sha2", + "utilities", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", +] + +[[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.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "towntalk" +version = "0.1.0" +dependencies = [ + "candid", + "futures", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "paginator", + "serde", + "utilities", +] + +[[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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "user" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "serde", +] + +[[package]] +name = "utilities" +version = "0.1.0" +dependencies = [ + "candid", + "ic-cdk", + "ic-cdk-timers", + "ic_principal", + "serde", + "time", + "uuid", +] + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..82a5c06 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,28 @@ +dfx start --clean --background + +npm i + +cargo clean + +cargo build + +dfx deps pull + +dfx canister create --all + +# if [ "${IS_LOCAL_DEV:-}" = "true" ]; then +# fi + +bash ./scripts/deploy_ledger.sh --with-redeem --temp-acc + +dfx build + +dfx deps deploy + +canister_names=$(jq -r '.canisters | keys[]' "dfx.json" | grep -v -E '^(icp_ledger_canister|icrc1_ledger_canister|internet_identity)$') + +for canister in $canister_names; do + dfx deploy $canister +done + +# dfx canister install --all \ No newline at end of file diff --git a/deps/pulled.json b/deps/pulled.json index 665eda5..df4c35f 100644 --- a/deps/pulled.json +++ b/deps/pulled.json @@ -10,4 +10,4 @@ "gzip": true } } -} +} \ No newline at end of file diff --git a/dfx.json b/dfx.json index 42b14b4..425ca6e 100644 --- a/dfx.json +++ b/dfx.json @@ -98,6 +98,21 @@ "internet_identity": { "type": "pull", "id": "rdmx6-jaaaa-aaaaa-aaadq-cai" + }, + "icrc1_ledger_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/e915efecc8af90993ccfc499721ebe826aadba60/rs/ledger_suite/icrc1/ledger/ledger.did", + "wasm": "https://download.dfinity.systems/ic/e915efecc8af90993ccfc499721ebe826aadba60/canisters/ic-icrc1-ledger.wasm.gz" + }, + "icp_ledger_canister": { + "type": "custom", + "candid": "https://raw.githubusercontent.com/dfinity/ic/e915efecc8af90993ccfc499721ebe826aadba60/rs/ledger_suite/icp/ledger.did", + "wasm": "https://download.dfinity.systems/ic/e915efecc8af90993ccfc499721ebe826aadba60/canisters/ledger-canister.wasm.gz", + "remote": { + "id": { + "ic": "ryjl3-tyaaa-aaaaa-aaaba-cai" + } + } } }, "output_env_file": ".env", diff --git a/package-lock.json b/package-lock.json index 3208f51..b73b760 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,14 @@ "workspaces": [ "src/frontend" ], + "dependencies": { + "@dfinity/agent": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/ledger-icp": "^2.6.13", + "@dfinity/ledger-icrc": "^2.9.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/utils": "^2.13.2" + }, "devDependencies": { "@dfinity/pic": "^0.13.1", "@testing-library/user-event": "^14.6.1", @@ -479,7 +487,6 @@ "resolved": "https://registry.npmjs.org/@dfinity/agent/-/agent-2.4.1.tgz", "integrity": "sha512-IczFFOUDGfMTdQ83yiCvGtvHr1IIB80lWBP0ZYRLogs6NVt8t6HYcMlu1sgT+9VivhT7iwX4pktPFxxOkO3COw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.1", @@ -511,7 +518,6 @@ "resolved": "https://registry.npmjs.org/@dfinity/candid/-/candid-2.4.1.tgz", "integrity": "sha512-kOaIKfhR2PYN8vD4M0Pc4s/7wb1nKjlTJUw+5E9jh26T03fITIZmaafIuwlX+wmdxwIT9Xoy7PlsxOEpzv203A==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "@dfinity/principal": "^2.4.1" } @@ -532,6 +538,30 @@ "@dfinity/principal": "^2.4.1" } }, + "node_modules/@dfinity/ledger-icp": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icp/-/ledger-icp-2.6.13.tgz", + "integrity": "sha512-Py/fpmT7+LVhrGYgKapuPz2l0+6HoVvuyN6xCr6MAu6DA8375cWJmXKeCWLz5qWPRu+D7zjKeitWdhUfxAy/cA==", + "license": "Apache-2.0", + "peerDependencies": { + "@dfinity/agent": "^2.0.0", + "@dfinity/candid": "^2.0.0", + "@dfinity/principal": "^2.0.0", + "@dfinity/utils": "^2.13.0" + } + }, + "node_modules/@dfinity/ledger-icrc": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@dfinity/ledger-icrc/-/ledger-icrc-2.9.1.tgz", + "integrity": "sha512-3qqCPoohzBJDymawb5PI8m+wj98N6FMpfHIVFjUmkx93usRPHgy3ZeOVnQi1E6hkZ9mhFv2dMDsfh/IXnOOtvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@dfinity/agent": "^2.0.0", + "@dfinity/candid": "^2.0.0", + "@dfinity/principal": "^2.0.0", + "@dfinity/utils": "^2.13.0" + } + }, "node_modules/@dfinity/pic": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@dfinity/pic/-/pic-0.13.1.tgz", @@ -554,11 +584,21 @@ "resolved": "https://registry.npmjs.org/@dfinity/principal/-/principal-2.4.1.tgz", "integrity": "sha512-Cz6XQVOwq0TXDBClPbcidDd4SqK1lfr1/Kn34ruDD13xVQ4iaP1iCntzS9O97+vGpY/6jwDtKd32Gn5YJ9BQNw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@noble/hashes": "^1.3.1" } }, + "node_modules/@dfinity/utils": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@dfinity/utils/-/utils-2.13.2.tgz", + "integrity": "sha512-D9Gkvtj59NQavSClOJjr7xp5cBEahK4ekrTFCFKfdm51V7HHbOnoyTx5vjcWg5x4vdi9dRD6EtEO9UoCp3b7gQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@dfinity/agent": "^2.0.0", + "@dfinity/candid": "^2.0.0", + "@dfinity/principal": "^2.0.0" + } + }, "node_modules/@dimforge/rapier3d-compat": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", @@ -1128,7 +1168,6 @@ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", "license": "MIT", - "peer": true, "dependencies": { "@noble/hashes": "1.8.0" }, @@ -3062,7 +3101,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==", - "peer": true, "engines": { "node": ">= 0.6.0" } @@ -3101,7 +3139,6 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", - "peer": true, "engines": { "node": "*" } @@ -3133,7 +3170,6 @@ "resolved": "https://registry.npmjs.org/borc/-/borc-2.1.2.tgz", "integrity": "sha512-Sy9eoUi4OiKzq7VovMn246iTo17kzuyHJKomCfpWMlI6RpfN1gk95w7d7gH264nApVLg0HZfcpz62/g4VH1Y4w==", "license": "MIT", - "peer": true, "dependencies": { "bignumber.js": "^9.0.0", "buffer": "^5.5.0", @@ -3166,7 +3202,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -3425,8 +3460,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -3625,8 +3659,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/delimit-stream/-/delimit-stream-0.1.0.tgz", "integrity": "sha512-a02fiQ7poS5CnjiJBAsjGLPp5EwVoGHNeu9sziBd9huppRfsAFIpv5zNLv0V1gbop53ilngAf5Kf331AwcoRBQ==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/dequal": { "version": "2.0.3", @@ -4173,8 +4206,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/install": { "version": "0.13.0", @@ -4275,7 +4307,6 @@ "resolved": "https://registry.npmjs.org/iso-url/-/iso-url-0.4.7.tgz", "integrity": "sha512-27fFRDnPAMnHGLq36bWTpKET+eiXct3ENlCcdcMdk+mjXrb2kw3mhBUg1B7ewAC0kVzlOPhADzQgz1SE6Tglog==", "license": "MIT", - "peer": true, "engines": { "node": ">=10" } @@ -4409,7 +4440,6 @@ "resolved": "https://registry.npmjs.org/json-text-sequence/-/json-text-sequence-0.1.1.tgz", "integrity": "sha512-L3mEegEWHRekSHjc7+sc8eJhba9Clq1PZ8kMkzf8OxElhXc8O4TS5MwcVlj9aEbm5dr81N90WHC5nAz3UO971w==", "license": "MIT", - "peer": true, "dependencies": { "delimit-stream": "0.1.0" } @@ -7903,7 +7933,6 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8078,8 +8107,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -8166,8 +8194,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/simple-cbor/-/simple-cbor-0.4.1.tgz", "integrity": "sha512-rijcxtwx2b4Bje3sqeIqw5EeW7UlOIC4YfOdwqIKacpvRQ/D78bWg/4/0m5e0U91oKvlGh7LlJuZCu07ISCC7w==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/sonner": { "version": "2.0.6", @@ -8233,7 +8260,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -9336,7 +9362,13 @@ "src/frontend": { "version": "1.0.0", "dependencies": { + "@dfinity/agent": "^2.4.1", "@dfinity/auth-client": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/ledger-icp": "^2.6.13", + "@dfinity/ledger-icrc": "^2.9.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/utils": "^2.13.2", "@hookform/resolvers": "^5.1.1", "@peculiar/webcrypto": "^1.5.0", "@radix-ui/react-avatar": "^1.1.10", diff --git a/package.json b/package.json index 0972a73..d567a8a 100644 --- a/package.json +++ b/package.json @@ -32,5 +32,13 @@ "vite": "^6.2.1", "vite-plugin-environment": "^1.1.3", "vitest": "^3.1.2" + }, + "dependencies": { + "@dfinity/agent": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/ledger-icp": "^2.6.13", + "@dfinity/ledger-icrc": "^2.9.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/utils": "^2.13.2" } } diff --git a/src/cry-token/deploy_ledger.bash b/scripts/deploy_ledger.sh similarity index 87% rename from src/cry-token/deploy_ledger.bash rename to scripts/deploy_ledger.sh index 131d021..de4b860 100644 --- a/src/cry-token/deploy_ledger.bash +++ b/scripts/deploy_ledger.sh @@ -2,15 +2,13 @@ set -euo pipefail # Import .env variables -if [ -f .bash.env ]; then +if [ -f .env ]; then set -a - source .bash.env + source .env set +a fi # Initialize default values and flags -ICP_OWNER="$DEPLOY_ID" -CRY_OWNER="$DEPLOY_ID" TEMP_ACC=false SET_OWNERS=false WITH_REDEEM=false @@ -160,7 +158,7 @@ echo "Setting up a new Minting account: " dfx identity use "$DEPLOYER_IDENTITY" - dfx start --clean --background + # dfx start --clean --background echo "Temporary accounts set up successfully!" @@ -202,33 +200,33 @@ echo "===============================" ARG_PRESET_CANISTER_PRINCIPAL="" if [ "${IS_LOCAL_DEV:-}" = "true" ]; then - ARG_PRESET_CANISTER_PRINCIPAL="--specified-id ${PRESET_CANISTER_PRINCIPAL}" + ARG_PRESET_CANISTER_PRINCIPAL="--specified-id $CANISTER_ID_CRY_LEDGER_CANISTER" dfx extension install nns - # Run dfx nns install in background with progress indicator - echo "Installing NNS (this may take a few minutes)..." - dfx nns install > nns_install.log 2>&1 & - NNS_PID=$! + # # Run dfx nns install in background with progress indicator + # echo "Installing NNS (this may take a few minutes)..." + # dfx nns install > nns_install.log 2>&1 & + # NNS_PID=$! - # Show progress while waiting - echo -n "Progress: " - while kill -0 $NNS_PID 2>/dev/null; do - echo -n "." - sleep 2 - done - echo " Done!" + # # Show progress while waiting + # echo -n "Progress: " + # while kill -0 $NNS_PID 2>/dev/null; do + # echo -n "." + # sleep 2 + # done + # echo " Done!" - # Wait for the process to complete and check exit status - wait $NNS_PID - NNS_EXIT_CODE=$? + # # Wait for the process to complete and check exit status + # wait $NNS_PID + # NNS_EXIT_CODE=$? - if [ $NNS_EXIT_CODE -eq 0 ]; then - echo "NNS installation completed successfully!" - else - echo "NNS installation failed. Check nns_install.log for details." - exit 1 - fi + # if [ $NNS_EXIT_CODE -eq 0 ]; then + # echo "NNS installation completed successfully!" + # else + # echo "NNS installation failed. Check nns_install.log for details." + # exit 1 + # fi dfx deploy --specified-id ryjl3-tyaaa-aaaaa-aaaba-cai icp_ledger_canister --argument " (variant { @@ -254,7 +252,7 @@ if [ "${IS_LOCAL_DEV:-}" = "true" ]; then fi -dfx deploy icrc1_ledger_canister $ARG_PRESET_CANISTER_PRINCIPAL --argument "( +dfx deploy --specified-id $CANISTER_ID_CRY_LEDGER_CANISTER icrc1_ledger_canister --argument "( variant {Init = record { token_symbol = \"${TOKEN_SYMBOL}\"; @@ -274,10 +272,3 @@ dfx deploy icrc1_ledger_canister $ARG_PRESET_CANISTER_PRINCIPAL --argument "( } )" -# Run a process after the script is terminated -# Example: trap a function on EXIT -cleanup() { - -} - -trap cleanup EXIT \ No newline at end of file diff --git a/scripts/wheretoken.bash b/scripts/wheretoken.bash new file mode 100644 index 0000000..58b980b --- /dev/null +++ b/scripts/wheretoken.bash @@ -0,0 +1,68 @@ +#!/bin/bash +# filepath: c:\Users\jason\Documents\programming-project\Fullstack\ICP\Overworked\scripts\wheretoken.bash + +if [ -f .env ]; then + set -a + source .env + set +a +fi + +# Get all identities +IDENTITIES=$(dfx identity list) + +echo "Account balances of CRY token (ICRC1):" +echo "======================================" + +for IDENTITY in $IDENTITIES; do + echo "Checking identity: $IDENTITY" + + # Switch to the identity + dfx identity use "$IDENTITY" >/dev/null 2>&1 + + # Get the principal ID + PRINCIPAL=$(dfx identity get-principal) + + # Get the balance using ICRC1 method + echo " Principal: $PRINCIPAL" + + # Use ICRC1 balance_of method with proper account format + BALANCE=$(dfx canister call $CANISTER_ID_ICRC1_LEDGER_CANISTER icrc1_balance_of "(record { owner = principal \"$PRINCIPAL\"; subaccount = null })" 2>&1) + + if [ $? -eq 0 ]; then + echo " Balance: $BALANCE" + else + echo " Error getting balance: $BALANCE" + fi + + echo " ---" +done + +echo "" +echo "Account balances of ICP token (Legacy ledger):" +echo "=============================================" + +for IDENTITY in $IDENTITIES; do + echo "Checking identity: $IDENTITY" + + # Switch to the identity + dfx identity use "$IDENTITY" >/dev/null 2>&1 + + # Get the account-id (hex) + ACCOUNT_ID=$(dfx ledger account-id) + + # Convert hex account-id to vec{...} format for legacy ledger + VEC_FORMAT=$(python3 -c "print('vec{' + ';'.join([str(b) for b in bytes.fromhex('$ACCOUNT_ID')]) + '}')") + + echo " Account ID: $ACCOUNT_ID" + + # Get the balance from ICP ledger + BALANCE=$(dfx canister call $CANISTER_ID_ICP_LEDGER_CANISTER account_balance "(record { account = $VEC_FORMAT })" 2>&1) + + if [ $? -eq 0 ]; then + echo " Balance: $BALANCE" + else + echo " Error getting balance: $BALANCE" + fi + + echo " ---" +done \ No newline at end of file diff --git a/src/backend/course/src/lib.rs b/src/backend/course/src/lib.rs index 6a071c1..526db4e 100644 --- a/src/backend/course/src/lib.rs +++ b/src/backend/course/src/lib.rs @@ -1,634 +1,634 @@ -use candid::CandidType; -use ic_cdk::{api, export_candid}; -use ic_principal::Principal; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::collections::HashMap; - -// pub mod user; - -// use user::{get_all_users, register_user, User}; -// use user::{User}; -// use user_canister_api::{Service, User}; - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Course { - pub id: u64, - pub instructor_id: u64, // Links to the instructor (User) - pub category_id: u64, // Links to the category - pub title: String, - pub description: String, - pub price: f32, - pub language: String, // Language of instruction - pub average_rating: f32, // 1.0 to 5.0 - pub created_at: u64, - pub updated_at: u64, - pub completed: bool, - pub image: String, - pub category: String, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateCourseInput { - pub instructor_id: u64, - pub category_id: u64, - pub title: String, - pub description: String, - pub price: f32, - pub language: String, - pub completed: bool, - pub image: String, - pub category: String, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Enrollment { - pub enrollment_id: u64, - pub user_id: Principal, // Links to the user - pub course_id: u64, // Links to the course - pub enrolled_at: u64, // Unix timestamp (ms) for enrollment - pub progress: f32, // Progress percentage (0.0 to 100.0) -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateEnrollmentInput { - pub user_id: Principal, - pub course_id: u64, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Module { - pub module_id: u64, - pub course_id: u64, - pub title: String, - pub description: String, - pub position: u32, - pub completed: bool, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateModuleInput { - pub course_id: u64, - pub title: String, - pub position: u32, - pub description: String, - pub completed: bool, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Lecture { - pub lecture_id: u64, - pub module_id: u64, - pub title: String, - pub content_url: String, - pub duration: u32, - pub position: u32, - pub description: String, - pub completed: bool, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateLectureInput { - pub module_id: u64, - pub title: String, - pub content_url: String, - pub duration: u32, - pub position: u32, - pub description: String, - pub completed: bool, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Instructor { - pub instructor_id: u64, - pub user_id: Principal, - pub full_name: String, - pub email: String, - pub phone: String, - pub country: String, - pub city: String, - pub profile_image: String, - pub bio: String, - pub expertise: String, - pub experience: String, - pub education: String, - pub portfolio: String, - pub linkedin: String, - pub video: String, - pub why: String, - pub ideas: String, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateInstructorInput { - pub user_id: Principal, - pub full_name: String, - pub email: String, - pub phone: String, - pub country: String, - pub city: String, - pub profile_image: String, - pub bio: String, - pub expertise: String, - pub experience: String, - pub education: String, - pub portfolio: String, - pub linkedin: String, - pub video: String, - pub why: String, - pub ideas: String, -} - -#[derive(Default, Serialize, Deserialize)] -pub struct CanisterState { - pub courses: HashMap, - pub enrollments: HashMap, - pub modules: HashMap, - pub lectures: HashMap, - pub instructors: HashMap, -} - -// thread_local! { -// pub static COURSES: RefCell = RefCell::new(CanisterState::default()); -// pub static ENROLLMENTS: RefCell = RefCell::new(CanisterState::default()); -// pub static LECTURES: RefCell = RefCell::new(CanisterState::default()); -// pub static MODULES: RefCell = RefCell::new(CanisterState::default()); -// } - -thread_local! { - pub static STATE: RefCell = RefCell::new(CanisterState::default()); -} - -pub fn generate_id(map: &std::collections::HashMap) -> u64 { - (map.len() as u64) + 1 -} - -fn now() -> u64 { - api::time() / 1_000_000 -} - -#[ic_cdk::update] -pub fn create_enrollment(input: CreateEnrollmentInput) -> Enrollment { - STATE.with(|state| { - let mut state = state.borrow_mut(); - let temp_id = generate_id(&state.enrollments); - let timestamp = now(); - - let enrollment = Enrollment { - enrollment_id: temp_id, - user_id: input.user_id, - course_id: input.course_id, - enrolled_at: timestamp, - progress: 0.0, - }; - - state.enrollments.insert(temp_id, enrollment.clone()); - enrollment - }) -} - -#[ic_cdk::query] -pub fn get_all_enrollments() -> Vec { - STATE.with(|state| { - let state = state.borrow(); - state.enrollments.values().cloned().collect() - }) -} - -#[ic_cdk::update] -pub fn create_course(input: CreateCourseInput) -> Course { - STATE.with(|state| { - let mut state = state.borrow_mut(); - let course_id = generate_id(&state.courses); - let timestamp = now(); - - let course = Course { - id: course_id, - instructor_id: input.instructor_id, - category_id: input.category_id, - title: input.title, - description: input.description, - price: input.price, - language: input.language, - average_rating: 0.0, - created_at: timestamp, - updated_at: timestamp, - completed: false, - image: input.image, - category: input.category, - }; - - state.courses.insert(course_id, course.clone()); - course - }) -} - -#[ic_cdk::query] -pub fn get_all_courses() -> Vec { - STATE.with(|state| { - let state = state.borrow(); - state.courses.values().cloned().collect() - }) -} - -#[ic_cdk::query] -pub fn get_course_by_id(course_id: u64) -> Option { - STATE.with(|state| { - let state = state.borrow(); - state.courses.get(&course_id).cloned() - }) -} - -#[ic_cdk::update] -pub fn create_lecture(input: CreateLectureInput) -> Lecture { - STATE.with(|state| { - let mut state = state.borrow_mut(); - let temp_id = generate_id(&state.lectures); - - let lecture = Lecture { - lecture_id: temp_id, - module_id: input.module_id, - title: input.title, - content_url: input.content_url, - duration: input.duration, - position: input.position, - description: input.description, - completed: false, - }; - - state.lectures.insert(temp_id, lecture.clone()); - lecture - }) -} - -#[ic_cdk::query] -pub fn get_all_lectures() -> Vec { - STATE.with(|state| { - let state = state.borrow(); - state.lectures.values().cloned().collect() - }) -} - -#[ic_cdk::update] -pub fn create_module(input: CreateModuleInput) -> Module { - STATE.with(|state| { - let mut state = state.borrow_mut(); - let temp_id = generate_id(&state.modules); - - let module = Module { - module_id: temp_id, - course_id: input.course_id, - title: input.title, - position: input.position, - description: input.description, - completed: false, - }; - - state.modules.insert(temp_id, module.clone()); - module - }) -} - -#[ic_cdk::query] -pub fn get_all_modules() -> Vec { - STATE.with(|state| { - let state = state.borrow(); - state.modules.values().cloned().collect() - }) -} - -#[ic_cdk::update] -pub fn create_instructor(input: CreateInstructorInput) -> Instructor { - STATE.with(|state| { - let mut state = state.borrow_mut(); - let temp_id = generate_id(&state.instructors); - - let instructor = Instructor { - instructor_id: temp_id, - user_id: input.user_id, - full_name: input.full_name, - email: input.email, - phone: input.phone, - country: input.country, - city: input.city, - profile_image: input.profile_image, - bio: input.bio, - expertise: input.expertise, - experience: input.experience, - education: input.education, - portfolio: input.portfolio, - linkedin: input.linkedin, - video: input.video, - why: input.why, - ideas: input.ideas, - }; - - state.instructors.insert(temp_id, instructor.clone()); - instructor - }) -} - -#[ic_cdk::query] -pub fn get_all_instructors() -> Vec { - STATE.with(|state| { - let state = state.borrow(); - state.instructors.values().cloned().collect() - }) -} - -#[ic_cdk::query] -pub fn get_instructor_by_id(instructor_id: u64) -> Option { - STATE.with(|state| { - let state = state.borrow(); - state.instructors.get(&instructor_id).cloned() - }) -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct CourseFullContent { - pub id: u64, - pub instructor_id: u64, - pub category_id: u64, - pub title: String, - pub description: String, - pub price: f32, - pub language: String, - pub average_rating: f32, - pub created_at: u64, - pub updated_at: u64, - pub completed: bool, - pub modules: Vec, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct ModuleWithLectures { - pub module_id: u64, - pub course_id: u64, - pub title: String, - pub description: String, - pub position: u32, - pub completed: bool, - pub lectures: Vec, -} - -#[ic_cdk::query] -pub fn get_course_with_modules_and_lectures(course_id: u64) -> Option { - STATE.with(|course_state| { - let course_state = course_state.borrow(); - let course = course_state.courses.get(&course_id).cloned(); - - if let Some(course) = course { - STATE.with(|module_state| { - let module_state = module_state.borrow(); - - // Get all modules belonging to the course - let course_modules: Vec = module_state - .modules - .values() - .filter(|m| m.course_id == course_id) - .cloned() - .collect(); - - STATE.with(|lecture_state| { - let lecture_state = lecture_state.borrow(); - - // For each module, collect its lectures - let modules_with_lectures: Vec = course_modules - .into_iter() - .map(|module| { - let lectures: Vec = lecture_state - .lectures - .values() - .filter(|l| l.module_id == module.module_id) - .cloned() - .collect(); - - ModuleWithLectures { - module_id: module.module_id, - course_id: module.course_id, - title: module.title, - description: module.description, - position: module.position, - completed: module.completed, - lectures, - } - }) - .collect(); - - Some(CourseFullContent { - id: course.id, - instructor_id: course.instructor_id, - category_id: course.category_id, - title: course.title, - description: course.description, - price: course.price, - language: course.language, - average_rating: course.average_rating, - created_at: course.created_at, - updated_at: course.updated_at, - completed: course.completed, - modules: modules_with_lectures, - }) - }) - }) - } else { - None - } - }) -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct CourseOverview { - pub id: u64, - pub instructor_id: u64, - pub category_id: u64, - pub title: String, - pub description: String, - pub price: f32, - pub language: String, - pub average_rating: f32, - pub created_at: u64, - pub updated_at: u64, - pub completed: bool, - pub modules: Vec, - pub instructor: Instructor, -} - -#[ic_cdk::query] -pub fn get_course_with_instructor_and_modules(course_id: u64) -> Option { - STATE.with(|state| { - let state = state.borrow(); - - let course = state.courses.get(&course_id).cloned()?; - - let modules: Vec = state - .modules - .values() - .filter(|m| m.course_id == course_id) - .cloned() - .collect(); - - let instructor = state.instructors.get(&course.instructor_id).cloned()?; - - Some(CourseOverview { - id: course.id, - instructor_id: course.instructor_id, - category_id: course.category_id, - title: course.title, - description: course.description, - price: course.price, - language: course.language, - average_rating: course.average_rating, - created_at: course.created_at, - updated_at: course.updated_at, - completed: course.completed, - modules, - instructor, - }) - }) -} - -#[ic_cdk::update] -fn seed_all() { - let demo_courses = vec![ - Course { - id: 1, - // instructor_id: Principal::from_text("2vxsx-fae").unwrap(), - instructor_id: 1, - category_id: 101, - title: "Rust Programming".to_string(), - description: "Learn Rust from scratch!".to_string(), - price: 49.99, - language: "English".to_string(), - average_rating: 4.8, - created_at: 1_625_000_000, - updated_at: 1_625_500_000, - completed: false, - image: "/images/placeholder/rust.png".to_string(), - category: "Coding".to_string(), - }, - Course { - id: 2, - // instructor_id: Principal::from_text("4gxsx-hdf").unwrap(), - instructor_id: 2, - category_id: 102, - title: "Introduction to Web Development".to_string(), - description: - "A beginner-friendly guide to web development using HTML, CSS, and JavaScript." - .to_string(), - price: 39.99, - language: "English".to_string(), - average_rating: 4.5, - created_at: 1_626_000_000, - updated_at: 1_626_500_000, - completed: false, - image: "/images/placeholder/webdev.png".to_string(), - category: "Web Development".to_string(), - }, - Course { - id: 3, - // instructor_id: Principal::from_text("7ahqx-tqe").unwrap(), - instructor_id: 3, - category_id: 103, - title: "Data Science with Python".to_string(), - description: "Analyze data, build models, and create visualizations with Python." - .to_string(), - price: 59.99, - language: "English".to_string(), - average_rating: 4.7, - created_at: 1_627_000_000, - updated_at: 1_627_500_000, - completed: false, - image: "/images/placeholder/datascience.png".to_string(), - category: "Data Science".to_string(), - }, - Course { - id: 4, - instructor_id: 4, - category_id: 104, - title: "Graphic Design Basics".to_string(), - description: - "Master the basics of graphic design with tools like Photoshop and Illustrator." - .to_string(), - price: 29.99, - language: "English".to_string(), - average_rating: 4.3, - created_at: 1_628_000_000, - updated_at: 1_628_500_000, - completed: false, - image: "/images/placeholder/design.png".to_string(), - category: "Design".to_string(), - }, - Course { - id: 5, - instructor_id: 5, - category_id: 105, - title: "Business Strategy Fundamentals".to_string(), - description: "Understand the key concepts behind successful business strategies." - .to_string(), - price: 44.99, - language: "English".to_string(), - average_rating: 4.6, - created_at: 1_629_000_000, - updated_at: 1_629_500_000, - completed: false, - image: "/images/placeholder/business.png".to_string(), - category: "Business".to_string(), - }, - ]; - - STATE.with(|state| { - let mut state = state.borrow_mut(); - for course in &demo_courses { - state.courses.insert(course.id, course.clone()); - } - }); - - let demo_modules = vec![Module { - module_id: 1, - course_id: 1, - title: "Introduction".to_string(), - description: "Intro to Rust".to_string(), - position: 1, - completed: false, - }]; - - STATE.with(|state| { - let mut state = state.borrow_mut(); - for module in &demo_modules { - state.modules.insert(module.module_id, module.clone()); - } - }); - - let demo_lectures = vec![ - Lecture { - lecture_id: 1, - module_id: 1, - title: "What is Rust?".to_string(), - content_url: "https://example.com/rust-intro".to_string(), - duration: 600, - position: 1, - description: "An overview of Rust language".to_string(), - completed: false, - }, - Lecture { - lecture_id: 2, - module_id: 1, - title: "Setting Up Environment".to_string(), - content_url: "https://example.com/setup".to_string(), - duration: 900, - position: 2, - description: "Installing Rust and tools".to_string(), - completed: false, - }, - ]; - - STATE.with(|state| { - let mut state = state.borrow_mut(); - for lecture in &demo_lectures { - state.lectures.insert(lecture.lecture_id, lecture.clone()); - } - }); -} - -export_candid!(); +use candid::CandidType; +use ic_cdk::{api, export_candid}; +use ic_principal::Principal; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashMap; + +// pub mod user; + +// use user::{get_all_users, register_user, User}; +// use user::{User}; +// use user_canister_api::{Service, User}; + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Course { + pub id: u64, + pub instructor_id: u64, // Links to the instructor (User) + pub category_id: u64, // Links to the category + pub title: String, + pub description: String, + pub price: f32, + pub language: String, // Language of instruction + pub average_rating: f32, // 1.0 to 5.0 + pub created_at: u64, + pub updated_at: u64, + pub completed: bool, + pub image: String, + pub category: String, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateCourseInput { + pub instructor_id: u64, + pub category_id: u64, + pub title: String, + pub description: String, + pub price: f32, + pub language: String, + pub completed: bool, + pub image: String, + pub category: String, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Enrollment { + pub enrollment_id: u64, + pub user_id: Principal, // Links to the user + pub course_id: u64, // Links to the course + pub enrolled_at: u64, // Unix timestamp (ms) for enrollment + pub progress: f32, // Progress percentage (0.0 to 100.0) +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateEnrollmentInput { + pub user_id: Principal, + pub course_id: u64, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Module { + pub module_id: u64, + pub course_id: u64, + pub title: String, + pub description: String, + pub position: u32, + pub completed: bool, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateModuleInput { + pub course_id: u64, + pub title: String, + pub position: u32, + pub description: String, + pub completed: bool, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Lecture { + pub lecture_id: u64, + pub module_id: u64, + pub title: String, + pub content_url: String, + pub duration: u32, + pub position: u32, + pub description: String, + pub completed: bool, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateLectureInput { + pub module_id: u64, + pub title: String, + pub content_url: String, + pub duration: u32, + pub position: u32, + pub description: String, + pub completed: bool, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Instructor { + pub instructor_id: u64, + pub user_id: Principal, + pub full_name: String, + pub email: String, + pub phone: String, + pub country: String, + pub city: String, + pub profile_image: String, + pub bio: String, + pub expertise: String, + pub experience: String, + pub education: String, + pub portfolio: String, + pub linkedin: String, + pub video: String, + pub why: String, + pub ideas: String, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateInstructorInput { + pub user_id: Principal, + pub full_name: String, + pub email: String, + pub phone: String, + pub country: String, + pub city: String, + pub profile_image: String, + pub bio: String, + pub expertise: String, + pub experience: String, + pub education: String, + pub portfolio: String, + pub linkedin: String, + pub video: String, + pub why: String, + pub ideas: String, +} + +#[derive(Default, Serialize, Deserialize)] +pub struct CanisterState { + pub courses: HashMap, + pub enrollments: HashMap, + pub modules: HashMap, + pub lectures: HashMap, + pub instructors: HashMap, +} + +// thread_local! { +// pub static COURSES: RefCell = RefCell::new(CanisterState::default()); +// pub static ENROLLMENTS: RefCell = RefCell::new(CanisterState::default()); +// pub static LECTURES: RefCell = RefCell::new(CanisterState::default()); +// pub static MODULES: RefCell = RefCell::new(CanisterState::default()); +// } + +thread_local! { + pub static STATE: RefCell = RefCell::new(CanisterState::default()); +} + +pub fn generate_id(map: &std::collections::HashMap) -> u64 { + (map.len() as u64) + 1 +} + +fn now() -> u64 { + api::time() / 1_000_000 +} + +#[ic_cdk::update] +pub fn create_enrollment(input: CreateEnrollmentInput) -> Enrollment { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let temp_id = generate_id(&state.enrollments); + let timestamp = now(); + + let enrollment = Enrollment { + enrollment_id: temp_id, + user_id: input.user_id, + course_id: input.course_id, + enrolled_at: timestamp, + progress: 0.0, + }; + + state.enrollments.insert(temp_id, enrollment.clone()); + enrollment + }) +} + +#[ic_cdk::query] +pub fn get_all_enrollments() -> Vec { + STATE.with(|state| { + let state = state.borrow(); + state.enrollments.values().cloned().collect() + }) +} + +#[ic_cdk::update] +pub fn create_course(input: CreateCourseInput) -> Course { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let course_id = generate_id(&state.courses); + let timestamp = now(); + + let course = Course { + id: course_id, + instructor_id: input.instructor_id, + category_id: input.category_id, + title: input.title, + description: input.description, + price: input.price, + language: input.language, + average_rating: 0.0, + created_at: timestamp, + updated_at: timestamp, + completed: false, + image: input.image, + category: input.category, + }; + + state.courses.insert(course_id, course.clone()); + course + }) +} + +#[ic_cdk::query] +pub fn get_all_courses() -> Vec { + STATE.with(|state| { + let state = state.borrow(); + state.courses.values().cloned().collect() + }) +} + +#[ic_cdk::query] +pub fn get_course_by_id(course_id: u64) -> Option { + STATE.with(|state| { + let state = state.borrow(); + state.courses.get(&course_id).cloned() + }) +} + +#[ic_cdk::update] +pub fn create_lecture(input: CreateLectureInput) -> Lecture { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let temp_id = generate_id(&state.lectures); + + let lecture = Lecture { + lecture_id: temp_id, + module_id: input.module_id, + title: input.title, + content_url: input.content_url, + duration: input.duration, + position: input.position, + description: input.description, + completed: false, + }; + + state.lectures.insert(temp_id, lecture.clone()); + lecture + }) +} + +#[ic_cdk::query] +pub fn get_all_lectures() -> Vec { + STATE.with(|state| { + let state = state.borrow(); + state.lectures.values().cloned().collect() + }) +} + +#[ic_cdk::update] +pub fn create_module(input: CreateModuleInput) -> Module { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let temp_id = generate_id(&state.modules); + + let module = Module { + module_id: temp_id, + course_id: input.course_id, + title: input.title, + position: input.position, + description: input.description, + completed: false, + }; + + state.modules.insert(temp_id, module.clone()); + module + }) +} + +#[ic_cdk::query] +pub fn get_all_modules() -> Vec { + STATE.with(|state| { + let state = state.borrow(); + state.modules.values().cloned().collect() + }) +} + +#[ic_cdk::update] +pub fn create_instructor(input: CreateInstructorInput) -> Instructor { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let temp_id = generate_id(&state.instructors); + + let instructor = Instructor { + instructor_id: temp_id, + user_id: input.user_id, + full_name: input.full_name, + email: input.email, + phone: input.phone, + country: input.country, + city: input.city, + profile_image: input.profile_image, + bio: input.bio, + expertise: input.expertise, + experience: input.experience, + education: input.education, + portfolio: input.portfolio, + linkedin: input.linkedin, + video: input.video, + why: input.why, + ideas: input.ideas, + }; + + state.instructors.insert(temp_id, instructor.clone()); + instructor + }) +} + +#[ic_cdk::query] +pub fn get_all_instructors() -> Vec { + STATE.with(|state| { + let state = state.borrow(); + state.instructors.values().cloned().collect() + }) +} + +#[ic_cdk::query] +pub fn get_instructor_by_id(instructor_id: u64) -> Option { + STATE.with(|state| { + let state = state.borrow(); + state.instructors.get(&instructor_id).cloned() + }) +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct CourseFullContent { + pub id: u64, + pub instructor_id: u64, + pub category_id: u64, + pub title: String, + pub description: String, + pub price: f32, + pub language: String, + pub average_rating: f32, + pub created_at: u64, + pub updated_at: u64, + pub completed: bool, + pub modules: Vec, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct ModuleWithLectures { + pub module_id: u64, + pub course_id: u64, + pub title: String, + pub description: String, + pub position: u32, + pub completed: bool, + pub lectures: Vec, +} + +#[ic_cdk::query] +pub fn get_course_with_modules_and_lectures(course_id: u64) -> Option { + STATE.with(|course_state| { + let course_state = course_state.borrow(); + let course = course_state.courses.get(&course_id).cloned(); + + if let Some(course) = course { + STATE.with(|module_state| { + let module_state = module_state.borrow(); + + // Get all modules belonging to the course + let course_modules: Vec = module_state + .modules + .values() + .filter(|m| m.course_id == course_id) + .cloned() + .collect(); + + STATE.with(|lecture_state| { + let lecture_state = lecture_state.borrow(); + + // For each module, collect its lectures + let modules_with_lectures: Vec = course_modules + .into_iter() + .map(|module| { + let lectures: Vec = lecture_state + .lectures + .values() + .filter(|l| l.module_id == module.module_id) + .cloned() + .collect(); + + ModuleWithLectures { + module_id: module.module_id, + course_id: module.course_id, + title: module.title, + description: module.description, + position: module.position, + completed: module.completed, + lectures, + } + }) + .collect(); + + Some(CourseFullContent { + id: course.id, + instructor_id: course.instructor_id, + category_id: course.category_id, + title: course.title, + description: course.description, + price: course.price, + language: course.language, + average_rating: course.average_rating, + created_at: course.created_at, + updated_at: course.updated_at, + completed: course.completed, + modules: modules_with_lectures, + }) + }) + }) + } else { + None + } + }) +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct CourseOverview { + pub id: u64, + pub instructor_id: u64, + pub category_id: u64, + pub title: String, + pub description: String, + pub price: f32, + pub language: String, + pub average_rating: f32, + pub created_at: u64, + pub updated_at: u64, + pub completed: bool, + pub modules: Vec, + pub instructor: Instructor, +} + +#[ic_cdk::query] +pub fn get_course_with_instructor_and_modules(course_id: u64) -> Option { + STATE.with(|state| { + let state = state.borrow(); + + let course = state.courses.get(&course_id).cloned()?; + + let modules: Vec = state + .modules + .values() + .filter(|m| m.course_id == course_id) + .cloned() + .collect(); + + let instructor = state.instructors.get(&course.instructor_id).cloned()?; + + Some(CourseOverview { + id: course.id, + instructor_id: course.instructor_id, + category_id: course.category_id, + title: course.title, + description: course.description, + price: course.price, + language: course.language, + average_rating: course.average_rating, + created_at: course.created_at, + updated_at: course.updated_at, + completed: course.completed, + modules, + instructor, + }) + }) +} + +#[ic_cdk::update] +fn seed_all() { + let demo_courses = vec![ + Course { + id: 1, + // instructor_id: Principal::from_text("2vxsx-fae").unwrap(), + instructor_id: 1, + category_id: 101, + title: "Rust Programming".to_string(), + description: "Learn Rust from scratch!".to_string(), + price: 49.99, + language: "English".to_string(), + average_rating: 4.8, + created_at: 1_625_000_000, + updated_at: 1_625_500_000, + completed: false, + image: "/images/placeholder/rust.png".to_string(), + category: "Coding".to_string(), + }, + Course { + id: 2, + // instructor_id: Principal::from_text("4gxsx-hdf").unwrap(), + instructor_id: 2, + category_id: 102, + title: "Introduction to Web Development".to_string(), + description: + "A beginner-friendly guide to web development using HTML, CSS, and JavaScript." + .to_string(), + price: 39.99, + language: "English".to_string(), + average_rating: 4.5, + created_at: 1_626_000_000, + updated_at: 1_626_500_000, + completed: false, + image: "/images/placeholder/webdev.png".to_string(), + category: "Web Development".to_string(), + }, + Course { + id: 3, + // instructor_id: Principal::from_text("7ahqx-tqe").unwrap(), + instructor_id: 3, + category_id: 103, + title: "Data Science with Python".to_string(), + description: "Analyze data, build models, and create visualizations with Python." + .to_string(), + price: 59.99, + language: "English".to_string(), + average_rating: 4.7, + created_at: 1_627_000_000, + updated_at: 1_627_500_000, + completed: false, + image: "/images/placeholder/datascience.png".to_string(), + category: "Data Science".to_string(), + }, + Course { + id: 4, + instructor_id: 4, + category_id: 104, + title: "Graphic Design Basics".to_string(), + description: + "Master the basics of graphic design with tools like Photoshop and Illustrator." + .to_string(), + price: 29.99, + language: "English".to_string(), + average_rating: 4.3, + created_at: 1_628_000_000, + updated_at: 1_628_500_000, + completed: false, + image: "/images/placeholder/design.png".to_string(), + category: "Design".to_string(), + }, + Course { + id: 5, + instructor_id: 5, + category_id: 105, + title: "Business Strategy Fundamentals".to_string(), + description: "Understand the key concepts behind successful business strategies." + .to_string(), + price: 44.99, + language: "English".to_string(), + average_rating: 4.6, + created_at: 1_629_000_000, + updated_at: 1_629_500_000, + completed: false, + image: "/images/placeholder/business.png".to_string(), + category: "Business".to_string(), + }, + ]; + + STATE.with(|state| { + let mut state = state.borrow_mut(); + for course in &demo_courses { + state.courses.insert(course.id, course.clone()); + } + }); + + let demo_modules = vec![Module { + module_id: 1, + course_id: 1, + title: "Introduction".to_string(), + description: "Intro to Rust".to_string(), + position: 1, + completed: false, + }]; + + STATE.with(|state| { + let mut state = state.borrow_mut(); + for module in &demo_modules { + state.modules.insert(module.module_id, module.clone()); + } + }); + + let demo_lectures = vec![ + Lecture { + lecture_id: 1, + module_id: 1, + title: "What is Rust?".to_string(), + content_url: "https://example.com/rust-intro".to_string(), + duration: 600, + position: 1, + description: "An overview of Rust language".to_string(), + completed: false, + }, + Lecture { + lecture_id: 2, + module_id: 1, + title: "Setting Up Environment".to_string(), + content_url: "https://example.com/setup".to_string(), + duration: 900, + position: 2, + description: "Installing Rust and tools".to_string(), + completed: false, + }, + ]; + + STATE.with(|state| { + let mut state = state.borrow_mut(); + for lecture in &demo_lectures { + state.lectures.insert(lecture.lecture_id, lecture.clone()); + } + }); +} + +export_candid!(); diff --git a/src/backend/grindarena/src/lib.rs b/src/backend/grindarena/src/lib.rs index c674ba7..c76109c 100644 --- a/src/backend/grindarena/src/lib.rs +++ b/src/backend/grindarena/src/lib.rs @@ -1,603 +1,603 @@ -use candid::{CandidType, Principal}; -use ic_cdk::{api::msg_caller, export_candid}; -use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::collections::HashMap; -use utilities::{StoredFile, generate_uuid, get_files, now}; - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub enum Difficulty { - Beginner, - Intermediate, - Advanced, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct CompetitionBriefInformation { - pub id: String, - pub title: String, - pub description: String, - pub difficulty: Difficulty, - pub prize: u64, // Amount of CRY-tokens - pub category: String, - pub status: String, // "Hot" or "Normal" - pub rules: Vec, - pub started_at: String, - pub ended_at: String, - - pub participant_count: usize, - pub time_left: String, -} - -pub struct CompetitionDetailInformation { - pub id: String, - pub title: String, - pub description: String, - pub difficulty: Difficulty, - pub prize: u64, // Amount of CRY-tokens - pub category: String, - pub status: String, // "Hot" or "Normal" - pub rules: Vec, - pub started_at: String, - pub ended_at: String, - - pub participant_count: usize, - pub time_left: String, - - // coordinators - pub coordinators: Vec, - - // prizes - pub prizes: Vec, - - // timeline - pub timeline: Vec, - - // requirements - pub requirements: Vec, // Requirements to participate in the competition - - // judging criteria - pub judging_criteria: Vec, // Criteria used to judge the competition - - // resources - pub resources: Vec, // Resources provided for the competition (e.g., problem statements, guidelines) -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub struct JudgingCriteria { - pub competition_id: String, - pub criteria: String, // Description of the judging criteria - pub weight: u64, // Weight of this criteria in the overall score -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub struct TimelineEvent { - pub order: u64, // Order of the event in the timeline - pub competition_id: String, - pub title: String, - pub description: String, - pub timestamp: String, // Timestamp of the event, - pub status: String, // Status of the event (e.g., "Upcoming", "Ongoing", "Completed") -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub struct Prize { - pub order: u64, // Order of the prize (1st, 2nd, 3rd, etc.) - pub competition_id: String, // ID of the competition this prize belongs to - pub amount: u64, // Amount of CRY-tokens for the prize -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountVisibleInformation { - id: String, - username: String, - profile_picture: Option, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Account { - pub id: String, - pub user_id: Principal, - pub username: String, - pub profile_picture: Option, - pub created_at: String, - pub deleted_at: Option, - pub updated_at: Option, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Competition { - pub id: String, - pub title: String, - pub description: String, - pub difficulty: Difficulty, - pub prize: u64, // Amount of CRY-tokens - // pub category_id: String, - pub category: String, - pub status: String, // "Hot" or "Normal" - pub rules: Vec, - pub started_at: String, - pub ended_at: String, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Coordinator { - pub id: String, - pub account_id: String, - pub competition_id: String, -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Participant { - pub id: String, - pub account_id: String, - pub competition_id: String, - pub score: Option, // Score achieved by the user in the competition -} - -#[derive(Clone, Serialize, Deserialize, CandidType)] -pub struct Submission { - pub id: String, - pub participant_id: String, - pub submitted_at: Option, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateCompetitionInput { - pub title: String, - pub description: String, - // pub image: StoredFile, - pub difficulty: Difficulty, - pub prize: u64, // Amount of CRY-tokens - pub category: String, - pub status: String, // "Hot" or "Normal" - pub rules: Vec, - pub started_at: String, - pub ended_at: String, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateAccountInput { - pub username: String, - pub profile_picture: Option, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateCoordinatorInput { - pub account_id: String, - pub competition_id: String, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateParticipantInput { - pub account_id: String, - pub competition_id: String, -} - -#[derive(Serialize, Deserialize, CandidType)] -pub struct CreateSubmissionInput { - pub participant_id: String, - pub content: String, -} - -thread_local! { - static COMPETITIONS: RefCell> = RefCell::new(HashMap::new()); - static ACCOUNTS: RefCell> = RefCell::new(HashMap::new()); - static COORDINATORS: RefCell> = RefCell::new(HashMap::new()); - static PARTICIPANTS: RefCell> = RefCell::new(HashMap::new()); - static SUBMISSIONS: RefCell> = RefCell::new(HashMap::new()); -} - -// SEEDERS - -#[ic_cdk::update] -fn seeder_all() { - account_seeders(); - competition_seeders(); - coordinator_seeders(); - participant_seeders(); - submission_seeders(); -} - -#[ic_cdk::update] -fn competition_seeders() { - let demo_competitions = vec![ - Competition { - id: "comp1".to_string(), - title: "Beginner Coding Challenge".to_string(), - description: "A simple coding challenge for beginners.".to_string(), - difficulty: Difficulty::Beginner, - prize: 1000, - category: "coding".to_string(), - status: "Hot".to_string(), - rules: vec!["Rule 1".to_string(), "Rule 2".to_string()], - started_at: now(), // Example timestamp - ended_at: now(), // Example timestamp - }, - Competition { - id: "comp2".to_string(), - title: "Advanced Algorithm Contest".to_string(), - description: "An advanced contest for algorithm enthusiasts.".to_string(), - difficulty: Difficulty::Advanced, - prize: 5000, - category: "algorithms".to_string(), - status: "Normal".to_string(), - rules: vec!["Rule A".to_string(), "Rule B".to_string()], - started_at: now(), // Example timestamp - ended_at: now(), // Example timestamp - }, - ]; - - COMPETITIONS.with(|state| { - let mut state = state.borrow_mut(); - for competition in demo_competitions { - state.insert(competition.id.clone(), competition); - } - }); -} - -#[ic_cdk::update] -fn account_seeders() { - // These should match the demo users in the User canister seeder - let demo_users = vec![ - ("Bob", "2vxsx-fae"), - ("Charlie", "w7x7r-cok77-xa"), - ("Dana", "aaaaa-aa"), - ("Eve", "bbbbb-aa"), - ("Frank", "ccccc-aa"), - ]; - let now = now(); - let mut idx = 1; - for (username, principal_str) in demo_users { - let id = format!("acc{idx}"); - let user_id = Principal::from_text(principal_str).unwrap_or(Principal::anonymous()); - let account = Account { - id: id.clone(), - user_id, - username: username.to_string(), - profile_picture: None, - created_at: now.clone(), - deleted_at: None, - updated_at: None, - }; - // Insert into a global ACCOUNTS map (assume exists) - ACCOUNTS.with(|state| state.borrow_mut().insert(id.clone(), account)); - idx += 1; - } -} - -#[ic_cdk::update] -fn coordinator_seeders() { - // Example: assign first two accounts as coordinators for the two demo competitions - let demo_coordinators = vec![("acc1", "comp1"), ("acc2", "comp2")]; - let mut idx = 1; - for (account_id, competition_id) in demo_coordinators { - let id = format!("coord{idx}"); - let coordinator = Coordinator { - id: id.clone(), - account_id: account_id.to_string(), - competition_id: competition_id.to_string(), - }; - COORDINATORS.with(|state| state.borrow_mut().insert(id.clone(), coordinator)); - idx += 1; - } -} - -#[ic_cdk::update] -fn participant_seeders() { - // Example: assign all accounts as participants in both competitions - let account_ids = vec!["acc1", "acc2", "acc3", "acc4", "acc5"]; - let competition_ids = vec!["comp1", "comp2"]; - let mut idx = 1; - for account_id in &account_ids { - for competition_id in &competition_ids { - let id = format!("part{idx}"); - let participant = Participant { - id: id.clone(), - account_id: account_id.to_string(), - competition_id: competition_id.to_string(), - score: None, - }; - PARTICIPANTS.with(|state| state.borrow_mut().insert(id.clone(), participant)); - idx += 1; - } - } -} - -#[ic_cdk::update] -fn submission_seeders() { - // Example: each participant submits once - let mut idx = 1; - PARTICIPANTS.with(|state| { - for participant in state.borrow().values() { - let id = format!("sub{idx}"); - let submission = Submission { - id: id.clone(), - participant_id: participant.id.clone(), - submitted_at: Some(now()), - }; - SUBMISSIONS.with(|subs| subs.borrow_mut().insert(id.clone(), submission)); - idx += 1; - } - }); -} - -async fn get_profile_picture( - storage_canister_id: Principal, - profile_picture_id: String, -) -> Option { - let files = get_files(storage_canister_id, vec![profile_picture_id.clone()]).await; - files.first().cloned() -} - -// ACCOUNT - -#[ic_cdk::query] -fn get_all_accounts() -> Vec { - ACCOUNTS.with(|state| state.borrow().values().cloned().collect()) -} - -#[ic_cdk::update] -async fn create_account(input: CreateAccountInput, storage_canister_id: Principal) -> String { - let principal = msg_caller(); - - let account_id = generate_uuid(); - - // let profile_picture_id: String = upload_files(storage_canister_id, vec![]) - // .await - // .iter() - // .map(|(id, _, _)| id.clone()) - // .collect::>()[0] - // .clone(); - - let new_account: Account = Account { - id: account_id.clone(), - user_id: principal, - username: input.username, - // profile_picture: Some(profile_picture_id), - profile_picture: None, - created_at: now(), - deleted_at: None, - updated_at: None, - }; - - ACCOUNTS.with_borrow_mut(|state| { - state.insert(account_id.clone(), new_account); - }); - - account_id -} - -#[ic_cdk::query] -async fn get_user_accounts(storage_canister_id: Principal) -> Vec { - let principal: Principal = msg_caller(); - - let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { - account_map - .values() - .filter(|acc: &&Account| acc.user_id == principal) - .cloned() - .collect::>() - }); - - let mut result = Vec::new(); - for acc in accounts { - let profile_picture = match &acc.profile_picture { - Some(pic_id) => get_profile_picture(storage_canister_id, pic_id.clone()).await, - None => None, - }; - - result.push(AccountVisibleInformation { - id: acc.id.clone(), - username: acc.username.clone(), - profile_picture, - }); - } - - result -} - -#[ic_cdk::update] -fn verify_login(account_id: String) -> bool { - ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.values().find(|acc| *acc.id == account_id) { - Some(acc) => acc.user_id == msg_caller(), - None => false, - } - }) -} - -// COMPETITIONS - -#[ic_cdk::query] -fn get_all_competitions() -> Vec { - // COMPETITIONS.with(|state| state.borrow().values().cloned().collect()) - - let competitions = - COMPETITIONS.with_borrow(|state| state.values().cloned().collect::>()); - - let mut result = Vec::new(); - for comp in competitions { - let participant_count = PARTICIPANTS.with_borrow(|state| { - state - .values() - .filter(|p| p.competition_id == comp.id) - .count() - }); - - result.push(CompetitionBriefInformation { - id: comp.id.clone(), - title: comp.title.clone(), - description: comp.description.clone(), - difficulty: comp.difficulty.clone(), - prize: comp.prize, - category: comp.category.clone(), - status: comp.status.clone(), - rules: comp.rules.clone(), - started_at: comp.started_at.clone(), - ended_at: comp.ended_at.clone(), - participant_count, - // time_left: format!("{} seconds", comp.ended_at - comp.started_at), // Example calculation - time_left: "10".to_string(), - }); - } - - result -} - -#[ic_cdk::update] -async fn create_competition(input: CreateCompetitionInput) -> String { - let competition_id = generate_uuid(); - let new_competition = Competition { - id: competition_id.clone(), - category: input.category, - status: input.status, - description: input.description, - title: input.title, - difficulty: input.difficulty, - prize: input.prize, - rules: input.rules, - started_at: input.started_at, - ended_at: input.ended_at, - }; - - COMPETITIONS.with(|state| { - state - .borrow_mut() - .insert(competition_id.clone(), new_competition) - }); - - competition_id -} - -// COORDINATORS - -#[ic_cdk::query] -fn get_all_coordinators(competition_id: String) -> Vec { - COORDINATORS.with(|state| { - state - .borrow() - .values() - .filter(|p| p.competition_id == competition_id) - .cloned() - .collect() - }) -} - -#[ic_cdk::update] -fn create_coordinator(input: CreateCoordinatorInput) -> String { - let competition_exist = COMPETITIONS.with(|state| { - let state = state.borrow(); - state.contains_key(&input.competition_id) - }); - - if !competition_exist { - // ic_cdk::println!("Invalid competition."); - return "".to_string(); - }; - - let coordinator_id = generate_uuid(); - let new_coordinator = Coordinator { - id: coordinator_id.clone(), - account_id: input.account_id, - competition_id: input.competition_id, - }; - - COORDINATORS.with(|state| { - state - .borrow_mut() - .insert(coordinator_id.clone(), new_coordinator); - }); - - coordinator_id -} - -// PARTICIPANTS - -#[ic_cdk::query] -fn get_all_participants(competition_id: String) -> Vec { - PARTICIPANTS.with(|state| { - state - .borrow() - .values() - .filter(|p| p.competition_id == competition_id) - .cloned() - .collect() - }) -} - -#[ic_cdk::update] -async fn create_participant(input: CreateParticipantInput) -> String { - let competition_exist = COMPETITIONS.with(|state| { - let state = state.borrow(); - state.contains_key(&input.competition_id) - }); - - if !competition_exist { - // ic_cdk::println!("Invalid competition."); - return "".to_string(); - } - - let particant_id = generate_uuid(); - let new_participant = Participant { - id: particant_id.clone(), - account_id: input.account_id, - competition_id: input.competition_id, - score: None, - }; - - PARTICIPANTS.with(|state| { - state - .borrow_mut() - .insert(particant_id.clone(), new_participant); - }); - - particant_id -} - -// SUBMISSIONS - -#[ic_cdk::query] -fn get_all_submissions(competition_id: String) -> Vec { - SUBMISSIONS.with(|state| { - state - .borrow() - .values() - .filter(|s| { - let participants = get_all_participants(competition_id.clone()); - participants.iter().any(|p| p.id == s.participant_id) - }) - .cloned() - .collect() - }) -} - -#[ic_cdk::update] -async fn create_submission(input: CreateSubmissionInput) -> String { - // let competition_exist = COMPETITIONS.with(|state| { - // let state = state.borrow(); - // state.contains_key(&input.competition_id) - // }); - - // if !competition_exist { - // // ic_cdk::println!("Invalid competition."); - // return "".to_string(); - // } - - let submission_id = generate_uuid(); - let new_submission = Submission { - id: submission_id.clone(), - participant_id: input.participant_id, - submitted_at: Some(now()), - }; - - SUBMISSIONS.with(|state| { - state - .borrow_mut() - .insert(submission_id.clone(), new_submission) - }); - - submission_id -} - -export_candid!(); +use candid::{CandidType, Principal}; +use ic_cdk::{api::msg_caller, export_candid}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::collections::HashMap; +use utilities::{StoredFile, generate_uuid, get_files, now}; + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub enum Difficulty { + Beginner, + Intermediate, + Advanced, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct CompetitionBriefInformation { + pub id: String, + pub title: String, + pub description: String, + pub difficulty: Difficulty, + pub prize: u64, // Amount of CRY-tokens + pub category: String, + pub status: String, // "Hot" or "Normal" + pub rules: Vec, + pub started_at: String, + pub ended_at: String, + + pub participant_count: usize, + pub time_left: String, +} + +pub struct CompetitionDetailInformation { + pub id: String, + pub title: String, + pub description: String, + pub difficulty: Difficulty, + pub prize: u64, // Amount of CRY-tokens + pub category: String, + pub status: String, // "Hot" or "Normal" + pub rules: Vec, + pub started_at: String, + pub ended_at: String, + + pub participant_count: usize, + pub time_left: String, + + // coordinators + pub coordinators: Vec, + + // prizes + pub prizes: Vec, + + // timeline + pub timeline: Vec, + + // requirements + pub requirements: Vec, // Requirements to participate in the competition + + // judging criteria + pub judging_criteria: Vec, // Criteria used to judge the competition + + // resources + pub resources: Vec, // Resources provided for the competition (e.g., problem statements, guidelines) +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub struct JudgingCriteria { + pub competition_id: String, + pub criteria: String, // Description of the judging criteria + pub weight: u64, // Weight of this criteria in the overall score +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub struct TimelineEvent { + pub order: u64, // Order of the event in the timeline + pub competition_id: String, + pub title: String, + pub description: String, + pub timestamp: String, // Timestamp of the event, + pub status: String, // Status of the event (e.g., "Upcoming", "Ongoing", "Completed") +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub struct Prize { + pub order: u64, // Order of the prize (1st, 2nd, 3rd, etc.) + pub competition_id: String, // ID of the competition this prize belongs to + pub amount: u64, // Amount of CRY-tokens for the prize +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountVisibleInformation { + id: String, + username: String, + profile_picture: Option, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Account { + pub id: String, + pub user_id: Principal, + pub username: String, + pub profile_picture: Option, + pub created_at: String, + pub deleted_at: Option, + pub updated_at: Option, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Competition { + pub id: String, + pub title: String, + pub description: String, + pub difficulty: Difficulty, + pub prize: u64, // Amount of CRY-tokens + // pub category_id: String, + pub category: String, + pub status: String, // "Hot" or "Normal" + pub rules: Vec, + pub started_at: String, + pub ended_at: String, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Coordinator { + pub id: String, + pub account_id: String, + pub competition_id: String, +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Participant { + pub id: String, + pub account_id: String, + pub competition_id: String, + pub score: Option, // Score achieved by the user in the competition +} + +#[derive(Clone, Serialize, Deserialize, CandidType)] +pub struct Submission { + pub id: String, + pub participant_id: String, + pub submitted_at: Option, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateCompetitionInput { + pub title: String, + pub description: String, + // pub image: StoredFile, + pub difficulty: Difficulty, + pub prize: u64, // Amount of CRY-tokens + pub category: String, + pub status: String, // "Hot" or "Normal" + pub rules: Vec, + pub started_at: String, + pub ended_at: String, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateAccountInput { + pub username: String, + pub profile_picture: Option, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateCoordinatorInput { + pub account_id: String, + pub competition_id: String, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateParticipantInput { + pub account_id: String, + pub competition_id: String, +} + +#[derive(Serialize, Deserialize, CandidType)] +pub struct CreateSubmissionInput { + pub participant_id: String, + pub content: String, +} + +thread_local! { + static COMPETITIONS: RefCell> = RefCell::new(HashMap::new()); + static ACCOUNTS: RefCell> = RefCell::new(HashMap::new()); + static COORDINATORS: RefCell> = RefCell::new(HashMap::new()); + static PARTICIPANTS: RefCell> = RefCell::new(HashMap::new()); + static SUBMISSIONS: RefCell> = RefCell::new(HashMap::new()); +} + +// SEEDERS + +#[ic_cdk::update] +fn seeder_all() { + account_seeders(); + competition_seeders(); + coordinator_seeders(); + participant_seeders(); + submission_seeders(); +} + +#[ic_cdk::update] +fn competition_seeders() { + let demo_competitions = vec![ + Competition { + id: "comp1".to_string(), + title: "Beginner Coding Challenge".to_string(), + description: "A simple coding challenge for beginners.".to_string(), + difficulty: Difficulty::Beginner, + prize: 1000, + category: "coding".to_string(), + status: "Hot".to_string(), + rules: vec!["Rule 1".to_string(), "Rule 2".to_string()], + started_at: now(), // Example timestamp + ended_at: now(), // Example timestamp + }, + Competition { + id: "comp2".to_string(), + title: "Advanced Algorithm Contest".to_string(), + description: "An advanced contest for algorithm enthusiasts.".to_string(), + difficulty: Difficulty::Advanced, + prize: 5000, + category: "algorithms".to_string(), + status: "Normal".to_string(), + rules: vec!["Rule A".to_string(), "Rule B".to_string()], + started_at: now(), // Example timestamp + ended_at: now(), // Example timestamp + }, + ]; + + COMPETITIONS.with(|state| { + let mut state = state.borrow_mut(); + for competition in demo_competitions { + state.insert(competition.id.clone(), competition); + } + }); +} + +#[ic_cdk::update] +fn account_seeders() { + // These should match the demo users in the User canister seeder + let demo_users = vec![ + ("Bob", "2vxsx-fae"), + ("Charlie", "w7x7r-cok77-xa"), + ("Dana", "aaaaa-aa"), + ("Eve", "bbbbb-aa"), + ("Frank", "ccccc-aa"), + ]; + let now = now(); + let mut idx = 1; + for (username, principal_str) in demo_users { + let id = format!("acc{idx}"); + let user_id = Principal::from_text(principal_str).unwrap_or(Principal::anonymous()); + let account = Account { + id: id.clone(), + user_id, + username: username.to_string(), + profile_picture: None, + created_at: now.clone(), + deleted_at: None, + updated_at: None, + }; + // Insert into a global ACCOUNTS map (assume exists) + ACCOUNTS.with(|state| state.borrow_mut().insert(id.clone(), account)); + idx += 1; + } +} + +#[ic_cdk::update] +fn coordinator_seeders() { + // Example: assign first two accounts as coordinators for the two demo competitions + let demo_coordinators = vec![("acc1", "comp1"), ("acc2", "comp2")]; + let mut idx = 1; + for (account_id, competition_id) in demo_coordinators { + let id = format!("coord{idx}"); + let coordinator = Coordinator { + id: id.clone(), + account_id: account_id.to_string(), + competition_id: competition_id.to_string(), + }; + COORDINATORS.with(|state| state.borrow_mut().insert(id.clone(), coordinator)); + idx += 1; + } +} + +#[ic_cdk::update] +fn participant_seeders() { + // Example: assign all accounts as participants in both competitions + let account_ids = vec!["acc1", "acc2", "acc3", "acc4", "acc5"]; + let competition_ids = vec!["comp1", "comp2"]; + let mut idx = 1; + for account_id in &account_ids { + for competition_id in &competition_ids { + let id = format!("part{idx}"); + let participant = Participant { + id: id.clone(), + account_id: account_id.to_string(), + competition_id: competition_id.to_string(), + score: None, + }; + PARTICIPANTS.with(|state| state.borrow_mut().insert(id.clone(), participant)); + idx += 1; + } + } +} + +#[ic_cdk::update] +fn submission_seeders() { + // Example: each participant submits once + let mut idx = 1; + PARTICIPANTS.with(|state| { + for participant in state.borrow().values() { + let id = format!("sub{idx}"); + let submission = Submission { + id: id.clone(), + participant_id: participant.id.clone(), + submitted_at: Some(now()), + }; + SUBMISSIONS.with(|subs| subs.borrow_mut().insert(id.clone(), submission)); + idx += 1; + } + }); +} + +async fn get_profile_picture( + storage_canister_id: Principal, + profile_picture_id: String, +) -> Option { + let files = get_files(storage_canister_id, vec![profile_picture_id.clone()]).await; + files.first().cloned() +} + +// ACCOUNT + +#[ic_cdk::query] +fn get_all_accounts() -> Vec { + ACCOUNTS.with(|state| state.borrow().values().cloned().collect()) +} + +#[ic_cdk::update] +async fn create_account(input: CreateAccountInput, storage_canister_id: Principal) -> String { + let principal = msg_caller(); + + let account_id = generate_uuid(); + + // let profile_picture_id: String = upload_files(storage_canister_id, vec![]) + // .await + // .iter() + // .map(|(id, _, _)| id.clone()) + // .collect::>()[0] + // .clone(); + + let new_account: Account = Account { + id: account_id.clone(), + user_id: principal, + username: input.username, + // profile_picture: Some(profile_picture_id), + profile_picture: None, + created_at: now(), + deleted_at: None, + updated_at: None, + }; + + ACCOUNTS.with_borrow_mut(|state| { + state.insert(account_id.clone(), new_account); + }); + + account_id +} + +#[ic_cdk::query] +async fn get_user_accounts(storage_canister_id: Principal) -> Vec { + let principal: Principal = msg_caller(); + + let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { + account_map + .values() + .filter(|acc: &&Account| acc.user_id == principal) + .cloned() + .collect::>() + }); + + let mut result = Vec::new(); + for acc in accounts { + let profile_picture = match &acc.profile_picture { + Some(pic_id) => get_profile_picture(storage_canister_id, pic_id.clone()).await, + None => None, + }; + + result.push(AccountVisibleInformation { + id: acc.id.clone(), + username: acc.username.clone(), + profile_picture, + }); + } + + result +} + +#[ic_cdk::update] +fn verify_login(account_id: String) -> bool { + ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.values().find(|acc| *acc.id == account_id) { + Some(acc) => acc.user_id == msg_caller(), + None => false, + } + }) +} + +// COMPETITIONS + +#[ic_cdk::query] +fn get_all_competitions() -> Vec { + // COMPETITIONS.with(|state| state.borrow().values().cloned().collect()) + + let competitions = + COMPETITIONS.with_borrow(|state| state.values().cloned().collect::>()); + + let mut result = Vec::new(); + for comp in competitions { + let participant_count = PARTICIPANTS.with_borrow(|state| { + state + .values() + .filter(|p| p.competition_id == comp.id) + .count() + }); + + result.push(CompetitionBriefInformation { + id: comp.id.clone(), + title: comp.title.clone(), + description: comp.description.clone(), + difficulty: comp.difficulty.clone(), + prize: comp.prize, + category: comp.category.clone(), + status: comp.status.clone(), + rules: comp.rules.clone(), + started_at: comp.started_at.clone(), + ended_at: comp.ended_at.clone(), + participant_count, + // time_left: format!("{} seconds", comp.ended_at - comp.started_at), // Example calculation + time_left: "10".to_string(), + }); + } + + result +} + +#[ic_cdk::update] +async fn create_competition(input: CreateCompetitionInput) -> String { + let competition_id = generate_uuid(); + let new_competition = Competition { + id: competition_id.clone(), + category: input.category, + status: input.status, + description: input.description, + title: input.title, + difficulty: input.difficulty, + prize: input.prize, + rules: input.rules, + started_at: input.started_at, + ended_at: input.ended_at, + }; + + COMPETITIONS.with(|state| { + state + .borrow_mut() + .insert(competition_id.clone(), new_competition) + }); + + competition_id +} + +// COORDINATORS + +#[ic_cdk::query] +fn get_all_coordinators(competition_id: String) -> Vec { + COORDINATORS.with(|state| { + state + .borrow() + .values() + .filter(|p| p.competition_id == competition_id) + .cloned() + .collect() + }) +} + +#[ic_cdk::update] +fn create_coordinator(input: CreateCoordinatorInput) -> String { + let competition_exist = COMPETITIONS.with(|state| { + let state = state.borrow(); + state.contains_key(&input.competition_id) + }); + + if !competition_exist { + // ic_cdk::println!("Invalid competition."); + return "".to_string(); + }; + + let coordinator_id = generate_uuid(); + let new_coordinator = Coordinator { + id: coordinator_id.clone(), + account_id: input.account_id, + competition_id: input.competition_id, + }; + + COORDINATORS.with(|state| { + state + .borrow_mut() + .insert(coordinator_id.clone(), new_coordinator); + }); + + coordinator_id +} + +// PARTICIPANTS + +#[ic_cdk::query] +fn get_all_participants(competition_id: String) -> Vec { + PARTICIPANTS.with(|state| { + state + .borrow() + .values() + .filter(|p| p.competition_id == competition_id) + .cloned() + .collect() + }) +} + +#[ic_cdk::update] +async fn create_participant(input: CreateParticipantInput) -> String { + let competition_exist = COMPETITIONS.with(|state| { + let state = state.borrow(); + state.contains_key(&input.competition_id) + }); + + if !competition_exist { + // ic_cdk::println!("Invalid competition."); + return "".to_string(); + } + + let particant_id = generate_uuid(); + let new_participant = Participant { + id: particant_id.clone(), + account_id: input.account_id, + competition_id: input.competition_id, + score: None, + }; + + PARTICIPANTS.with(|state| { + state + .borrow_mut() + .insert(particant_id.clone(), new_participant); + }); + + particant_id +} + +// SUBMISSIONS + +#[ic_cdk::query] +fn get_all_submissions(competition_id: String) -> Vec { + SUBMISSIONS.with(|state| { + state + .borrow() + .values() + .filter(|s| { + let participants = get_all_participants(competition_id.clone()); + participants.iter().any(|p| p.id == s.participant_id) + }) + .cloned() + .collect() + }) +} + +#[ic_cdk::update] +async fn create_submission(input: CreateSubmissionInput) -> String { + // let competition_exist = COMPETITIONS.with(|state| { + // let state = state.borrow(); + // state.contains_key(&input.competition_id) + // }); + + // if !competition_exist { + // // ic_cdk::println!("Invalid competition."); + // return "".to_string(); + // } + + let submission_id = generate_uuid(); + let new_submission = Submission { + id: submission_id.clone(), + participant_id: input.participant_id, + submitted_at: Some(now()), + }; + + SUBMISSIONS.with(|state| { + state + .borrow_mut() + .insert(submission_id.clone(), new_submission) + }); + + submission_id +} + +export_candid!(); diff --git a/src/backend/storage/src/lib.rs b/src/backend/storage/src/lib.rs index 1010a19..d4b25e6 100644 --- a/src/backend/storage/src/lib.rs +++ b/src/backend/storage/src/lib.rs @@ -1,599 +1,599 @@ -use std::{cell::RefCell, collections::HashMap}; - -use candid::CandidType; -use ic_cdk::api::msg_caller; -use ic_cdk::export_candid; -use ic_principal::Principal; -use paginator::{HasFields, Paginator, PaginatorResponse}; -use serde::{Deserialize, Serialize}; - -use utilities::{generate_uuid, now}; - -// Utils - -#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] -pub enum Access { - Owner, // for checking purpose only, not to be used as a substitute for the owner field. - Admin, - Write, - Read, - Delete, - Removed, - Public, -} - -#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] -pub enum FileUploadResolveType { - NotAuthorized, - SuccessfullyUploaded, - FailedToUpload, - AlreadyUploaded, -} - -impl Access { - pub fn all() -> Vec { - [ - Access::Admin, - Access::Write, - Access::Read, - Access::Delete, - Access::Public, - ] - .to_vec() - } - - pub fn can_edit() -> Vec { - [Access::Admin, Access::Write].to_vec() - } - - pub fn admin_access() -> Vec { - [Access::Admin, Access::Delete].to_vec() - } -} - -#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] -pub struct Group { - id: String, - name: String, - members: Vec<(Principal, Access)>, - owner: Principal, - public: bool, -} - -impl HasFields for Group { - fn get_field(&self, field_name: &str) -> String { - match field_name { - "id" => self.id.clone(), - "name" => self.name.clone(), - "owner" => self.owner.to_text(), - "public" => self.public.to_string(), - _ => "".to_string(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] -pub struct StoredFile { - id: String, - name: String, - mime_type: String, - size: usize, - data: Vec, - owner: Principal, - groups: Vec, - allowed_users: Vec<(Principal, Access)>, - public: bool, - uploaded_at: String, -} - -impl HasFields for StoredFile { - fn get_field(&self, field_name: &str) -> String { - match field_name { - "id" => self.id.clone(), - "name" => self.name.clone(), - "mime_type" => self.mime_type.clone(), - "size" => self.size.to_string(), - "owner" => self.owner.to_text(), - "public" => self.public.to_string(), - _ => "".to_string(), - } - } -} - -thread_local! { - static FILES: RefCell> = RefCell::new(HashMap::new()); - static GROUPS: RefCell> = RefCell::new(HashMap::new()); -} - -#[ic_cdk::query] -fn get_all() -> Vec { - FILES.with_borrow(|file_map| file_map.values().cloned().collect()) -} - -// Groups -#[ic_cdk::query] -fn check_group_permission( - group_id: String, - operations: Vec, - user: Option, -) -> bool { - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - let principal: Principal = user.unwrap_or(msg_caller()); - - match groups.get_mut(&group_id) { - Some(grp) => operations - .iter() - .any(|op| grp.owner == principal || grp.members.contains(&(principal, op.clone()))), - None => false, - } - }) -} - -#[ic_cdk::query] -fn create_group(group: Group) { - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - let principal: Principal = msg_caller(); - - let mut inserted_group = group.clone(); - - let id = generate_uuid(); - inserted_group.id = id.clone(); - inserted_group.owner = principal; - - groups.insert(id, group); - }); -} - -#[ic_cdk::query] -fn delete_groups(group_ids: Vec) -> usize { - let mut deleted_group_count: usize = 0; - - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - let principal: Principal = msg_caller(); - - for group_id in group_ids.iter() { - if let Some(grp) = groups.get_mut(group_id) { - if grp.owner == principal { - let was_deleted = GROUPS.with(|groups: &RefCell>| { - groups.borrow_mut().remove(group_id).is_some() - }); - - if was_deleted { - deleted_group_count += 1; - } - } - } - } - }); - - deleted_group_count -} - -#[ic_cdk::query] -fn remove_group_files(group_id: String, file_ids: Vec) -> usize { - let mut removed_file_count: usize = 0; - - if check_group_permission(group_id, Access::admin_access(), None) { - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - file_ids.iter().for_each(|file_id: &String| { - if groups.remove(file_id).is_some() { - removed_file_count += 1; - } - }); - }); - } - - removed_file_count -} - -#[ic_cdk::update] -fn assign_group_members( - users: Vec<(Principal, Access)>, - group_id: String, -) -> Result { - GROUPS.with_borrow_mut( - |groups: &mut HashMap| match groups.get_mut(&group_id) { - Some(grp) => { - let non_inserted_users: Vec<(Principal, Access)> = users - .iter() - .filter(|user: &&(Principal, Access)| { - !grp.members.contains(user) && user.1 != Access::Owner - }) - .cloned() - .collect(); - - if non_inserted_users.is_empty() { - return Ok("No new users to assign.".to_string()); - } - - if check_group_permission(group_id, Access::admin_access(), None) { - return Err("You are not authorized to assign members."); - } - - for user in non_inserted_users.clone() { - grp.members.push(user); - } - - Ok(format!("{} users assigned.", non_inserted_users.len())) - } - None => Err("Group not found."), - }, - ) -} - -#[ic_cdk::update] -fn edit_group_members(group_id: String, new_accesses: Vec<(Principal, Access)>) -> usize { - let mut updated_users: usize = 0; - - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - if let Some(group) = groups.get_mut(&group_id) { - let principal: Principal = msg_caller(); - - let is_owner = group.owner == principal; - let is_admin = !is_owner && group.members.contains(&(principal, Access::Admin)); - - if !is_owner && !is_admin { - return; - } - - let all_permissions = Access::all() - .iter() - .cloned() - .chain(std::iter::once(Access::Removed)) - .collect::>(); - - for (user, access) in new_accesses.iter() { - match access { - Access::Owner => {} - - Access::Removed => { - if group.members.contains(&(*user, Access::Admin)) && is_owner { - group.members.retain(|(u, _)| u != user); - updated_users += 1; - } else { - group.members.retain(|(u, a)| !(u == user && a == access)); - } - } - - Access::Admin => { - if is_owner && !group.members.contains(&(*user, Access::Admin)) { - group.members.push((*user, Access::Admin)); - updated_users += 1; - } - } - - _ => { - // For Read/Write - let already_has = all_permissions - .iter() - .any(|perm| group.members.contains(&(*user, perm.clone()))); - - if !already_has { - group.members.push((*user, access.clone())); - updated_users += 1; - } - } - } - } - } - }); - - updated_users -} - -#[ic_cdk::query] -fn get_group(group_id: String) -> Result { - GROUPS.with_borrow( - |groups: &HashMap| match groups.get(&group_id) { - Some(grp) => { - let principal: Principal = msg_caller(); - - if grp.owner == principal || grp.members.iter().any(|(user, _)| user == &principal) - { - Ok(grp.clone()) - } else { - Err("You do not have permission to view this group.") - } - } - None => Err("Group not found."), - }, - ) -} - -#[ic_cdk::query] -fn get_groups(page: usize, per_page: usize) -> PaginatorResponse { - let groups = GROUPS.with_borrow(|groups: &HashMap| { - let principal: Principal = msg_caller(); - groups - .values() - .filter(|grp: &&Group| { - grp.owner == principal || grp.members.iter().any(|(user, _)| user == &principal) - }) - .cloned() - .collect() - }); - - Paginator::new(groups, vec![]).get(page, per_page) -} - -// Files -#[ic_cdk::query] -fn check_file_permission( - file: StoredFile, - operations: Vec, - user: Option, -) -> bool { - let principal = user.unwrap_or(msg_caller()); - - if operations.contains(&Access::Owner) && file.owner == principal - || operations.contains(&Access::Public) && file.public - || operations - .iter() - .any(|op: &Access| file.allowed_users.contains(&(principal, op.clone()))) - { - return true; - } - - GROUPS.with_borrow_mut(|groups: &mut HashMap| { - groups.iter().any(|(_id, group): (&String, &Group)| { - operations - .iter() - .any(|op: &Access| group.members.contains(&(principal, op.clone()))) - }) - }) -} - -#[ic_cdk::update] -fn get_file(file_id: String, mutable: Option) -> Option { - FILES.with( - |files: &RefCell>| match files.borrow().get(&file_id) { - Some(file) => { - let access_requirements: Vec = if mutable.unwrap_or(false) { - Access::can_edit() - } else { - Access::all() - }; - - if check_file_permission(file.clone(), access_requirements, None) { - Some(file.clone()) - } else { - None - } - } - None => None, - }, - ) -} - -#[ic_cdk::update] -fn get_files_by_id(file_ids: Vec) -> Vec { - FILES.with_borrow(|file_map: &HashMap| { - file_map - .values() - .filter(|file: &&StoredFile| { - file_ids.contains(&file.id) - && check_file_permission((*file).clone(), vec![Access::Read], None) - }) - .cloned() - .collect::>() - }) -} - -#[ic_cdk::update] -fn get_files( - per_page: usize, - page: usize, - public: bool, - owned: bool, -) -> PaginatorResponse { - let my_files: Vec = FILES.with(|files: &RefCell>| { - if owned { - let principal: Principal = msg_caller(); - return files - .borrow() - .values() - .filter(|f| f.owner == principal) - .cloned() - .collect(); - } else if public { - return files - .borrow() - .values() - .filter(|f| f.public) - .cloned() - .collect(); - } else { - return files - .borrow() - .values() - .filter(|f| check_file_permission((*f).clone(), Access::all(), None)) - .cloned() - .collect(); - } - }); - - let paginator = Paginator::new(my_files, vec![]); - paginator.get(page, per_page) -} - -#[ic_cdk::update] -fn upload_files(files: Vec) -> Vec<(String, FileUploadResolveType, String)> { - let mut uploaded_files: Vec<(String, FileUploadResolveType, String)> = vec![]; - - let principal = msg_caller(); - - FILES.with_borrow_mut(|files_map: &mut HashMap| { - for file in files.iter() { - if !file.groups.is_empty() - && file.groups.iter().any(|grp: &Group| { - !check_group_permission(grp.id.clone(), Access::can_edit(), None) - }) - { - uploaded_files.push(( - file.name.clone(), - FileUploadResolveType::NotAuthorized, - "You are not authorized in this group.".to_string(), - )); - continue; - } - - let key: String = generate_uuid(); - - let mut inserted_file = file.clone(); - - inserted_file.id = key.clone(); - inserted_file.owner = principal; - inserted_file.uploaded_at = now(); - - if files_map - .insert(key.clone(), inserted_file.clone()) - .is_none() - { - uploaded_files.push(( - key, - FileUploadResolveType::SuccessfullyUploaded, - "Successfully uploaded.".to_string(), - )); - } else { - uploaded_files.push(( - key, - FileUploadResolveType::AlreadyUploaded, - "StoredFile is already uploaded.".to_string(), - )); - } - } - }); - - uploaded_files -} - -#[ic_cdk::update] -fn delete_files(file_ids: Vec) -> usize { - let mut deleted_file_count: usize = 0; - - for file_id in file_ids.iter() { - FILES.with_borrow_mut(|files_map: &mut HashMap| { - if let Some(f) = files_map.get_mut(file_id) { - let principal: Principal = msg_caller(); - if f.owner == principal { - let was_deleted = - FILES.with(|files| files.borrow_mut().remove(file_id).is_some()); - - if was_deleted { - deleted_file_count += 1; - } - } - } - }); - } - - deleted_file_count -} - -#[ic_cdk::update] -fn change_file_name(file_id: String, new_file_name: String) -> Result<&'static str, &'static str> { - FILES.with_borrow_mut( - |files: &mut HashMap| match files.get_mut(&file_id) { - Some(file) => { - if !check_file_permission(file.clone(), Access::can_edit(), None) { - return Err("You are not authorized to change this file name."); - } - - file.name = new_file_name; - Ok("StoredFile name changed successfully.") - } - None => Err("StoredFile not found."), - }, - ) -} - -#[ic_cdk::update] -fn edit_file_public_access( - file_id: String, - new_access: bool, -) -> Result<&'static str, &'static str> { - FILES.with_borrow_mut( - |files: &mut HashMap| match files.get_mut(&file_id) { - Some(file) => { - if !check_file_permission(file.clone(), Access::can_edit(), None) { - if new_access { - return Err("You are not authorized to publish this file."); - } else { - return Err("You are not authroized to unpublish this file."); - } - } - - file.public = new_access; - if new_access { - Ok("The file has been published successfully.") - } else { - Ok("The file has been unpublished successfully.") - } - } - None => Err("StoredFile not found."), - }, - ) -} - -#[ic_cdk::update] -fn edit_allowed_users(file_id: String, new_accesses: Vec<(Principal, Access)>) -> usize { - let mut updated_users: usize = 0; - - FILES.with_borrow_mut(|files: &mut HashMap| { - if let Some(file) = files.get_mut(&file_id) { - let principal: Principal = msg_caller(); - - let is_owner = file.owner == principal; - let is_admin = !is_owner && file.allowed_users.contains(&(principal, Access::Admin)); - - if !is_owner && !is_admin { - return; - } - - let all_permissions = Access::all() - .iter() - .cloned() - .chain(std::iter::once(Access::Removed)) - .collect::>(); - - for (user, access) in new_accesses.iter() { - match access { - Access::Owner => {} - - Access::Removed => { - if file.allowed_users.contains(&(*user, Access::Admin)) && is_owner { - file.allowed_users.retain(|(u, _)| u != user); - updated_users += 1; - } else { - file.allowed_users - .retain(|(u, a)| !(u == user && a == access)); - } - } - - Access::Admin => { - if is_owner && !file.allowed_users.contains(&(*user, Access::Admin)) { - file.allowed_users.push((*user, Access::Admin)); - updated_users += 1; - } - } - - _ => { - // For Read/Write - let already_has = all_permissions - .iter() - .any(|perm| file.allowed_users.contains(&(*user, perm.clone()))); - - if !already_has { - file.allowed_users.push((*user, access.clone())); - updated_users += 1; - } - } - } - } - } - }); - - updated_users -} - -export_candid!(); +use std::{cell::RefCell, collections::HashMap}; + +use candid::CandidType; +use ic_cdk::api::msg_caller; +use ic_cdk::export_candid; +use ic_principal::Principal; +use paginator::{HasFields, Paginator, PaginatorResponse}; +use serde::{Deserialize, Serialize}; + +use utilities::{generate_uuid, now}; + +// Utils + +#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub enum Access { + Owner, // for checking purpose only, not to be used as a substitute for the owner field. + Admin, + Write, + Read, + Delete, + Removed, + Public, +} + +#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub enum FileUploadResolveType { + NotAuthorized, + SuccessfullyUploaded, + FailedToUpload, + AlreadyUploaded, +} + +impl Access { + pub fn all() -> Vec { + [ + Access::Admin, + Access::Write, + Access::Read, + Access::Delete, + Access::Public, + ] + .to_vec() + } + + pub fn can_edit() -> Vec { + [Access::Admin, Access::Write].to_vec() + } + + pub fn admin_access() -> Vec { + [Access::Admin, Access::Delete].to_vec() + } +} + +#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] +pub struct Group { + id: String, + name: String, + members: Vec<(Principal, Access)>, + owner: Principal, + public: bool, +} + +impl HasFields for Group { + fn get_field(&self, field_name: &str) -> String { + match field_name { + "id" => self.id.clone(), + "name" => self.name.clone(), + "owner" => self.owner.to_text(), + "public" => self.public.to_string(), + _ => "".to_string(), + } + } +} + +#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] +pub struct StoredFile { + id: String, + name: String, + mime_type: String, + size: usize, + data: Vec, + owner: Principal, + groups: Vec, + allowed_users: Vec<(Principal, Access)>, + public: bool, + uploaded_at: String, +} + +impl HasFields for StoredFile { + fn get_field(&self, field_name: &str) -> String { + match field_name { + "id" => self.id.clone(), + "name" => self.name.clone(), + "mime_type" => self.mime_type.clone(), + "size" => self.size.to_string(), + "owner" => self.owner.to_text(), + "public" => self.public.to_string(), + _ => "".to_string(), + } + } +} + +thread_local! { + static FILES: RefCell> = RefCell::new(HashMap::new()); + static GROUPS: RefCell> = RefCell::new(HashMap::new()); +} + +#[ic_cdk::query] +fn get_all() -> Vec { + FILES.with_borrow(|file_map| file_map.values().cloned().collect()) +} + +// Groups +#[ic_cdk::query] +fn check_group_permission( + group_id: String, + operations: Vec, + user: Option, +) -> bool { + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + let principal: Principal = user.unwrap_or(msg_caller()); + + match groups.get_mut(&group_id) { + Some(grp) => operations + .iter() + .any(|op| grp.owner == principal || grp.members.contains(&(principal, op.clone()))), + None => false, + } + }) +} + +#[ic_cdk::query] +fn create_group(group: Group) { + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + let principal: Principal = msg_caller(); + + let mut inserted_group = group.clone(); + + let id = generate_uuid(); + inserted_group.id = id.clone(); + inserted_group.owner = principal; + + groups.insert(id, group); + }); +} + +#[ic_cdk::query] +fn delete_groups(group_ids: Vec) -> usize { + let mut deleted_group_count: usize = 0; + + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + let principal: Principal = msg_caller(); + + for group_id in group_ids.iter() { + if let Some(grp) = groups.get_mut(group_id) { + if grp.owner == principal { + let was_deleted = GROUPS.with(|groups: &RefCell>| { + groups.borrow_mut().remove(group_id).is_some() + }); + + if was_deleted { + deleted_group_count += 1; + } + } + } + } + }); + + deleted_group_count +} + +#[ic_cdk::query] +fn remove_group_files(group_id: String, file_ids: Vec) -> usize { + let mut removed_file_count: usize = 0; + + if check_group_permission(group_id, Access::admin_access(), None) { + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + file_ids.iter().for_each(|file_id: &String| { + if groups.remove(file_id).is_some() { + removed_file_count += 1; + } + }); + }); + } + + removed_file_count +} + +#[ic_cdk::update] +fn assign_group_members( + users: Vec<(Principal, Access)>, + group_id: String, +) -> Result { + GROUPS.with_borrow_mut( + |groups: &mut HashMap| match groups.get_mut(&group_id) { + Some(grp) => { + let non_inserted_users: Vec<(Principal, Access)> = users + .iter() + .filter(|user: &&(Principal, Access)| { + !grp.members.contains(user) && user.1 != Access::Owner + }) + .cloned() + .collect(); + + if non_inserted_users.is_empty() { + return Ok("No new users to assign.".to_string()); + } + + if check_group_permission(group_id, Access::admin_access(), None) { + return Err("You are not authorized to assign members."); + } + + for user in non_inserted_users.clone() { + grp.members.push(user); + } + + Ok(format!("{} users assigned.", non_inserted_users.len())) + } + None => Err("Group not found."), + }, + ) +} + +#[ic_cdk::update] +fn edit_group_members(group_id: String, new_accesses: Vec<(Principal, Access)>) -> usize { + let mut updated_users: usize = 0; + + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + if let Some(group) = groups.get_mut(&group_id) { + let principal: Principal = msg_caller(); + + let is_owner = group.owner == principal; + let is_admin = !is_owner && group.members.contains(&(principal, Access::Admin)); + + if !is_owner && !is_admin { + return; + } + + let all_permissions = Access::all() + .iter() + .cloned() + .chain(std::iter::once(Access::Removed)) + .collect::>(); + + for (user, access) in new_accesses.iter() { + match access { + Access::Owner => {} + + Access::Removed => { + if group.members.contains(&(*user, Access::Admin)) && is_owner { + group.members.retain(|(u, _)| u != user); + updated_users += 1; + } else { + group.members.retain(|(u, a)| !(u == user && a == access)); + } + } + + Access::Admin => { + if is_owner && !group.members.contains(&(*user, Access::Admin)) { + group.members.push((*user, Access::Admin)); + updated_users += 1; + } + } + + _ => { + // For Read/Write + let already_has = all_permissions + .iter() + .any(|perm| group.members.contains(&(*user, perm.clone()))); + + if !already_has { + group.members.push((*user, access.clone())); + updated_users += 1; + } + } + } + } + } + }); + + updated_users +} + +#[ic_cdk::query] +fn get_group(group_id: String) -> Result { + GROUPS.with_borrow( + |groups: &HashMap| match groups.get(&group_id) { + Some(grp) => { + let principal: Principal = msg_caller(); + + if grp.owner == principal || grp.members.iter().any(|(user, _)| user == &principal) + { + Ok(grp.clone()) + } else { + Err("You do not have permission to view this group.") + } + } + None => Err("Group not found."), + }, + ) +} + +#[ic_cdk::query] +fn get_groups(page: usize, per_page: usize) -> PaginatorResponse { + let groups = GROUPS.with_borrow(|groups: &HashMap| { + let principal: Principal = msg_caller(); + groups + .values() + .filter(|grp: &&Group| { + grp.owner == principal || grp.members.iter().any(|(user, _)| user == &principal) + }) + .cloned() + .collect() + }); + + Paginator::new(groups, vec![]).get(page, per_page) +} + +// Files +#[ic_cdk::query] +fn check_file_permission( + file: StoredFile, + operations: Vec, + user: Option, +) -> bool { + let principal = user.unwrap_or(msg_caller()); + + if operations.contains(&Access::Owner) && file.owner == principal + || operations.contains(&Access::Public) && file.public + || operations + .iter() + .any(|op: &Access| file.allowed_users.contains(&(principal, op.clone()))) + { + return true; + } + + GROUPS.with_borrow_mut(|groups: &mut HashMap| { + groups.iter().any(|(_id, group): (&String, &Group)| { + operations + .iter() + .any(|op: &Access| group.members.contains(&(principal, op.clone()))) + }) + }) +} + +#[ic_cdk::update] +fn get_file(file_id: String, mutable: Option) -> Option { + FILES.with( + |files: &RefCell>| match files.borrow().get(&file_id) { + Some(file) => { + let access_requirements: Vec = if mutable.unwrap_or(false) { + Access::can_edit() + } else { + Access::all() + }; + + if check_file_permission(file.clone(), access_requirements, None) { + Some(file.clone()) + } else { + None + } + } + None => None, + }, + ) +} + +#[ic_cdk::update] +fn get_files_by_id(file_ids: Vec) -> Vec { + FILES.with_borrow(|file_map: &HashMap| { + file_map + .values() + .filter(|file: &&StoredFile| { + file_ids.contains(&file.id) + && check_file_permission((*file).clone(), vec![Access::Read], None) + }) + .cloned() + .collect::>() + }) +} + +#[ic_cdk::update] +fn get_files( + per_page: usize, + page: usize, + public: bool, + owned: bool, +) -> PaginatorResponse { + let my_files: Vec = FILES.with(|files: &RefCell>| { + if owned { + let principal: Principal = msg_caller(); + return files + .borrow() + .values() + .filter(|f| f.owner == principal) + .cloned() + .collect(); + } else if public { + return files + .borrow() + .values() + .filter(|f| f.public) + .cloned() + .collect(); + } else { + return files + .borrow() + .values() + .filter(|f| check_file_permission((*f).clone(), Access::all(), None)) + .cloned() + .collect(); + } + }); + + let paginator = Paginator::new(my_files, vec![]); + paginator.get(page, per_page) +} + +#[ic_cdk::update] +fn upload_files(files: Vec) -> Vec<(String, FileUploadResolveType, String)> { + let mut uploaded_files: Vec<(String, FileUploadResolveType, String)> = vec![]; + + let principal = msg_caller(); + + FILES.with_borrow_mut(|files_map: &mut HashMap| { + for file in files.iter() { + if !file.groups.is_empty() + && file.groups.iter().any(|grp: &Group| { + !check_group_permission(grp.id.clone(), Access::can_edit(), None) + }) + { + uploaded_files.push(( + file.name.clone(), + FileUploadResolveType::NotAuthorized, + "You are not authorized in this group.".to_string(), + )); + continue; + } + + let key: String = generate_uuid(); + + let mut inserted_file = file.clone(); + + inserted_file.id = key.clone(); + inserted_file.owner = principal; + inserted_file.uploaded_at = now(); + + if files_map + .insert(key.clone(), inserted_file.clone()) + .is_none() + { + uploaded_files.push(( + key, + FileUploadResolveType::SuccessfullyUploaded, + "Successfully uploaded.".to_string(), + )); + } else { + uploaded_files.push(( + key, + FileUploadResolveType::AlreadyUploaded, + "StoredFile is already uploaded.".to_string(), + )); + } + } + }); + + uploaded_files +} + +#[ic_cdk::update] +fn delete_files(file_ids: Vec) -> usize { + let mut deleted_file_count: usize = 0; + + for file_id in file_ids.iter() { + FILES.with_borrow_mut(|files_map: &mut HashMap| { + if let Some(f) = files_map.get_mut(file_id) { + let principal: Principal = msg_caller(); + if f.owner == principal { + let was_deleted = + FILES.with(|files| files.borrow_mut().remove(file_id).is_some()); + + if was_deleted { + deleted_file_count += 1; + } + } + } + }); + } + + deleted_file_count +} + +#[ic_cdk::update] +fn change_file_name(file_id: String, new_file_name: String) -> Result<&'static str, &'static str> { + FILES.with_borrow_mut( + |files: &mut HashMap| match files.get_mut(&file_id) { + Some(file) => { + if !check_file_permission(file.clone(), Access::can_edit(), None) { + return Err("You are not authorized to change this file name."); + } + + file.name = new_file_name; + Ok("StoredFile name changed successfully.") + } + None => Err("StoredFile not found."), + }, + ) +} + +#[ic_cdk::update] +fn edit_file_public_access( + file_id: String, + new_access: bool, +) -> Result<&'static str, &'static str> { + FILES.with_borrow_mut( + |files: &mut HashMap| match files.get_mut(&file_id) { + Some(file) => { + if !check_file_permission(file.clone(), Access::can_edit(), None) { + if new_access { + return Err("You are not authorized to publish this file."); + } else { + return Err("You are not authroized to unpublish this file."); + } + } + + file.public = new_access; + if new_access { + Ok("The file has been published successfully.") + } else { + Ok("The file has been unpublished successfully.") + } + } + None => Err("StoredFile not found."), + }, + ) +} + +#[ic_cdk::update] +fn edit_allowed_users(file_id: String, new_accesses: Vec<(Principal, Access)>) -> usize { + let mut updated_users: usize = 0; + + FILES.with_borrow_mut(|files: &mut HashMap| { + if let Some(file) = files.get_mut(&file_id) { + let principal: Principal = msg_caller(); + + let is_owner = file.owner == principal; + let is_admin = !is_owner && file.allowed_users.contains(&(principal, Access::Admin)); + + if !is_owner && !is_admin { + return; + } + + let all_permissions = Access::all() + .iter() + .cloned() + .chain(std::iter::once(Access::Removed)) + .collect::>(); + + for (user, access) in new_accesses.iter() { + match access { + Access::Owner => {} + + Access::Removed => { + if file.allowed_users.contains(&(*user, Access::Admin)) && is_owner { + file.allowed_users.retain(|(u, _)| u != user); + updated_users += 1; + } else { + file.allowed_users + .retain(|(u, a)| !(u == user && a == access)); + } + } + + Access::Admin => { + if is_owner && !file.allowed_users.contains(&(*user, Access::Admin)) { + file.allowed_users.push((*user, Access::Admin)); + updated_users += 1; + } + } + + _ => { + // For Read/Write + let already_has = all_permissions + .iter() + .any(|perm| file.allowed_users.contains(&(*user, perm.clone()))); + + if !already_has { + file.allowed_users.push((*user, access.clone())); + updated_users += 1; + } + } + } + } + } + }); + + updated_users +} + +export_candid!(); diff --git a/src/backend/towntalk/Cargo.toml b/src/backend/towntalk/Cargo.toml index cae6a0b..0c9b9a8 100644 --- a/src/backend/towntalk/Cargo.toml +++ b/src/backend/towntalk/Cargo.toml @@ -1,17 +1,17 @@ -[package] -name = "towntalk" -version = "0.1.0" -edition = "2024" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -candid = "0.10" -futures = "0.3.31" -ic-cdk = "0.18.5" -ic-cdk-timers = "0.12.2" -ic_principal = "0.1.1" -serde = "1.0.219" -utilities = { path = "../utilities" } -paginator = { path = "../paginator" } +[package] +name = "towntalk" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +candid = "0.10" +futures = "0.3.31" +ic-cdk = "0.18.5" +ic-cdk-timers = "0.12.2" +ic_principal = "0.1.1" +serde = "1.0.219" +utilities = { path = "../utilities" } +paginator = { path = "../paginator" } diff --git a/src/backend/towntalk/src/lib.rs b/src/backend/towntalk/src/lib.rs index f330d4d..d8756c7 100644 --- a/src/backend/towntalk/src/lib.rs +++ b/src/backend/towntalk/src/lib.rs @@ -1,1073 +1,1073 @@ -use candid::{CandidType, Principal}; -use futures::future::join_all; -use ic_cdk::{api::msg_caller, export_candid}; -use paginator::{HasFields, Paginator, PaginatorResponse}; -use serde::{Deserialize, Serialize}; -use std::{cell::RefCell, collections::HashMap}; - -use utilities::{StoredFile, generate_uuid, get_files, now, upload_files}; - -#[derive(CandidType, Clone, Serialize, Deserialize, Debug)] -struct Comment { - id: String, - comment: String, - post_id: String, - poster_id: String, - replied_to: Option, - created_at: String, - updated_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct Post { - id: String, - poster_id: String, - title: String, - caption: String, - medias: Vec, - likes: Vec, - shares: Vec, - comments: Vec, - created_at: String, - updated_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct PostCreationPayload { - id: String, - poster_id: String, - title: String, - caption: String, - medias: Vec, - likes: Vec, - shares: Vec, - comments: Vec, - created_at: String, - updated_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct FeedPost { - id: String, - poster: AccountVisibleInformation, - title: String, - caption: String, - medias: Vec, - likes: Vec, - shares: Vec, - comments: Vec, - created_at: String, - updated_at: String, -} - -impl HasFields for FeedPost { - fn get_field(&self, field_name: &str) -> String { - match field_name { - "id" => self.id.clone(), - "poster" => self.poster.username.clone(), - "title" => self.title.clone(), - "caption" => self.caption.clone(), - "medias" => format!("{:?}", self.medias), - "likes" => format!("{:?}", self.likes), - "shares" => format!("{:?}", self.shares), - "comments" => format!("{:?}", self.comments), - "created_at" => self.created_at.clone(), - "updated_at" => self.updated_at.clone(), - _ => "".to_string(), - } - } -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountProfile { - username: String, - profile_picture: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct Account { - id: String, - user_id: Principal, - profile: AccountProfile, - followers: Vec<(String, String)>, - following: Vec<(String, String)>, - posts: Vec, - echos: Vec, - blocked: Vec<(String, String)>, - private: bool, - deleted_at: Option, - created_at: String, - updated_at: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct UserAccountProfile { - username: String, - profile_picture: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct UserAccount { - id: String, - user_id: Principal, - profile: UserAccountProfile, - followers: Vec<(String, String)>, - following: Vec<(String, String)>, - posts: Vec, - echos: Vec, - blocked: Vec<(String, String)>, - private: bool, - deleted_at: Option, - created_at: String, - updated_at: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountProfileCreationPayload { - username: String, - about: String, - profile_picture: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountCreationPayload { - profile: AccountProfileCreationPayload, - private: bool, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountDetails { - account: Account, - owned: bool, - posts: Option>, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountVisibleInformation { - id: String, - username: String, - profile_picture: Option, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct AccountDeletionPayload { - account_id: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct Echo { - id: String, - account_id: String, - media: Vec, - like: usize, - share: usize, - seen_by: Vec<(String, String)>, - created_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub enum LikableType { - POST, - STORY, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct Like { - account_id: String, - likable_id: String, - likable_type: LikableType, - created_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub enum ReportType { - SPAM, - TERRORISM, - SCAM, - PROFANITY, - HATESPEECH, - RACISM, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -pub enum ReportResolveType { - ACCOUNTSUSPENDED, - ACCOUNTDELETED, - USERSUSPENDED, - USERDELETED, - WARNING, - FALSE, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct Report { - id: String, - reporter_id: String, - reported_id: String, - report_type: Vec, - created_at: String, - resolved: Vec<(ReportResolveType, Option, String)>, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct FollowRequest { - requester_id: String, - requested_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct FollowRequestReturnPayload { - requester: AccountVisibleInformation, - requested_at: String, -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct ValidityCheckingPayload { - username: String, -} - -thread_local! { - static USER_ACCOUNTS: RefCell>> = RefCell::new(HashMap::new()); - static ACCOUNTS: RefCell> = RefCell::new(HashMap::new()); - static FOLLOW_REQUESTS: RefCell> = RefCell::new(HashMap::new()); - static POSTS: RefCell> = RefCell::new(HashMap::new()); - static LIKES: RefCell> = RefCell::new(HashMap::new()); - static COMMENTS: RefCell> = RefCell::new(HashMap::new()); - static ECHOS: RefCell> = RefCell::new(HashMap::new()); - static REPORTS: RefCell> = RefCell::new(HashMap::new()); -} - -// Accounts - -fn can_view(account_id: String, target_id: String) -> bool { - ACCOUNTS.with_borrow( - |accounts: &HashMap| match accounts.get(&target_id) { - Some(acc) => { - if is_owned(target_id) { - return true; - } - - let is_blocked: bool = acc - .blocked - .iter() - .any(|(blocked_id, _)| blocked_id == &account_id); - - if is_blocked { - return false; - } - - let is_private: bool = acc.private; - - if is_private { - acc.followers - .iter() - .any(|(follower_id, _)| follower_id == &account_id) - } else { - true - } - } - None => false, - }, - ) -} - -fn is_owned(account_id: String) -> bool { - let principal = msg_caller(); - ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.get(&account_id) { - Some(acc) => acc.user_id == principal, - None => false, - } - }) -} - -fn is_post_owner(post_id: String) -> bool { - POSTS.with_borrow(|post_map| match post_map.get(&post_id) { - Some(post) => ACCOUNTS.with_borrow(|account_map| match account_map.get(&post.poster_id) { - Some(acc) => { - let principal = msg_caller(); - acc.user_id == principal - } - None => false, - }), - None => false, - }) -} - -fn is_account_owner(account_id: String) -> bool { - ACCOUNTS.with_borrow(|account_map| match account_map.get(&account_id) { - Some(acc) => { - let principal = msg_caller(); - acc.user_id == principal - } - None => false, - }) -} - -fn is_comment_owner(account_id: String, comment_id: String) -> bool { - COMMENTS.with_borrow(|comment_map| match comment_map.get(&comment_id) { - Some(c) => is_account_owner(account_id.clone()) && c.poster_id == account_id, - None => false, - }) -} - -async fn get_profile_picture( - storage_canister_id: Principal, - profile_picture_id: String, -) -> Option { - let files = get_files(storage_canister_id, vec![profile_picture_id.clone()]).await; - files.first().cloned() -} - -async fn get_account_visible_information( - storage_canister_id: Principal, - target_id: String, -) -> Option { - let account_opt = ACCOUNTS - .with_borrow(|account_map: &HashMap| account_map.get(&target_id).cloned()); - - if let Some(acc) = account_opt { - let mut profile_picture = None; - - if let Some(pfp) = acc.profile.profile_picture.clone() { - profile_picture = get_profile_picture(storage_canister_id, pfp).await; - } - - Some(AccountVisibleInformation { - id: acc.id, - username: acc.profile.username, - profile_picture, - }) - } else { - None - } -} - -#[ic_cdk::query] -fn verify_login(account_id: String) -> bool { - ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.values().find(|acc| *acc.id == account_id) { - Some(acc) => acc.user_id == msg_caller(), - None => false, - } - }) -} - -#[ic_cdk::query] -fn check_validity(payload: ValidityCheckingPayload) -> bool { - ACCOUNTS.with_borrow(|account_map: &HashMap| { - let username_is_valid: bool = account_map - .values() - .all(|acc: &Account| acc.profile.username != payload.username); - - username_is_valid - }) -} - -#[ic_cdk::update] -async fn create_account( - payload: AccountCreationPayload, - storage_canister_id: Principal, -) -> Account { - let principal: Principal = msg_caller(); - - let account_id: String = generate_uuid(); - - let mut profile_picture_id: Option = None; - - if let Some(pfp) = payload.profile.profile_picture { - let upload_response = upload_files(storage_canister_id, vec![pfp]).await; - - if !upload_response.is_empty() { - profile_picture_id = Some( - upload_response - .iter() - .map(|(id, _, _)| id.clone()) - .collect::>()[0] - .clone(), - ); - } - } - - let account_data: Account = Account { - id: account_id.clone(), - user_id: principal, - followers: Vec::new(), - following: Vec::new(), - posts: Vec::new(), - echos: Vec::new(), - blocked: Vec::new(), - profile: AccountProfile { - username: payload.profile.username.clone(), - profile_picture: profile_picture_id, - }, - private: payload.private, - deleted_at: None, - created_at: now(), - updated_at: None, - }; - - ACCOUNTS.with_borrow_mut(|accounts: &mut HashMap| { - accounts.insert(account_id.clone(), account_data.clone()); - }); - - USER_ACCOUNTS.with_borrow_mut( - |user_account_map| match user_account_map.get_mut(&principal) { - Some(user_acc) => { - user_acc.push(account_id.clone()); - } - None => { - user_account_map.insert(principal, vec![account_id.clone()]); - } - }, - ); - - account_data -} - -#[ic_cdk::update] -async fn get_account(account_id: String, storage_canister_id: Principal) -> Option { - if let Some(account) = ACCOUNTS - .with_borrow(|account_map: &HashMap| account_map.get(&account_id).cloned()) - { - let profile_picture_id = account.profile.profile_picture.clone(); - let profile_picture = match profile_picture_id { - Some(ref pfp_id) => get_profile_picture(storage_canister_id, pfp_id.clone()).await, - None => None, - }; - - Some(UserAccount { - id: account.id.clone(), - user_id: account.user_id, - profile: UserAccountProfile { - username: account.profile.username.clone(), - profile_picture, - }, - followers: account.followers.clone(), - following: account.following.clone(), - posts: account.posts.clone(), - echos: account.echos.clone(), - blocked: account.blocked.clone(), - private: account.private, - deleted_at: account.deleted_at.clone(), - created_at: account.created_at.clone(), - updated_at: account.updated_at.clone(), - }) - } else { - None - } -} - -#[ic_cdk::update] -async fn get_user_accounts(storage_canister_id: Principal) -> Vec { - let principal: Principal = msg_caller(); - - let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { - account_map - .values() - .filter(|acc: &&Account| acc.user_id == principal) - .cloned() - .collect::>() - }); - - let mut result = Vec::new(); - for acc in accounts { - let mut profile_picture = None; - - if let Some(pfp) = acc.profile.profile_picture.clone() { - profile_picture = get_profile_picture(storage_canister_id, pfp).await; - } - - result.push(AccountVisibleInformation { - id: acc.id.clone(), - username: acc.profile.username.clone(), - profile_picture, - }); - } - result -} - -#[ic_cdk::update] -fn delete_account(payload: AccountDeletionPayload) { - let account_id = payload.account_id.clone(); - - ACCOUNTS.with_borrow_mut(|accounts: &mut HashMap| { - let account = accounts.get_mut(&account_id); - - if let Some(acc) = account { - if is_owned(account_id.clone()) { - acc.deleted_at = Some(now()); - } - } - }); - - let principal: Principal = msg_caller(); - - USER_ACCOUNTS.with_borrow_mut( - |user_account_map| match user_account_map.get_mut(&principal) { - Some(user_acc) => { - user_acc.retain(|acc_id| *acc_id != account_id); - } - None => { - user_account_map.retain(|user_principal, _| user_principal != &principal); - } - }, - ); -} - -#[ic_cdk::update] -fn report_account(payload: Report) { - let mut report_data: Report = payload; - report_data.id = generate_uuid(); - - REPORTS.with_borrow_mut(|reports: &mut HashMap| { - reports.insert(report_data.id.clone(), report_data); - }); -} - -#[ic_cdk::update] -fn block_account(account_id: String, target_id: String) { - ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { - if let Some(acc) = account_map.get_mut(&account_id) { - if is_owned(account_id) { - acc.blocked.push((target_id, now())); - } - } - }) -} - -#[ic_cdk::update] -fn unblock_account(account_id: String, target_id: String) { - ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { - if let Some(acc) = account_map.get_mut(&account_id) { - if is_owned(account_id) { - acc.blocked.retain(|(blocked, _)| blocked != &target_id); - } - } - }) -} - -#[ic_cdk::query] -fn get_account_details(account_id: String, target_id: String) -> Option { - let principal: Principal = msg_caller(); - - let target_account = ACCOUNTS - .with_borrow(|accounts: &HashMap| accounts.get(&target_id).cloned()); - - match target_account { - Some(acc) => { - let owned: bool = acc.user_id == principal; - if can_view(account_id, target_id.clone()) { - let posts = POSTS.with_borrow(|post_map| { - Some( - post_map - .values() - .filter(|post| post.poster_id == target_id.clone()) - .cloned() - .collect::>(), - ) - }); - - Some(AccountDetails { - account: acc, - owned, - posts, - }) - } else { - Some(AccountDetails { - account: acc, - owned, - posts: None, - }) - } - } - None => None, - } -} - -#[ic_cdk::query] -fn get_profile(account_id: String) -> Option { - if is_owned(account_id.clone()) { - ACCOUNTS - .with_borrow(|account_map| account_map.get(&account_id).map(|acc| acc.profile.clone())) - } else { - None - } -} - -#[ic_cdk::update] -fn follow(account_id: String, target_id: String) { - if is_owned(account_id.clone()) { - let account_id_cloned = account_id.clone(); - ACCOUNTS.with_borrow_mut(|account_map| { - if let Some(acc) = account_map.get_mut(&target_id) { - if can_view(account_id_cloned.clone(), acc.id.clone()) { - if acc.private { - // create follow request - FOLLOW_REQUESTS.with_borrow_mut(|request_map| { - request_map.insert( - target_id, - FollowRequest { - requester_id: account_id.clone(), - requested_at: now(), - }, - ); - }); - } else { - acc.followers.push((account_id_cloned.clone(), now())); - } - } - } - }); - } -} - -#[ic_cdk::update] -fn unfollow(account_id: String, target_id: String) -> f32 { - if is_owned(account_id.clone()) { - ACCOUNTS.with_borrow_mut(|account_map| match account_map.get_mut(&target_id) { - Some(acc) => { - acc.followers.retain(|(a, _)| a != &account_id); - if acc.private { 0.0 } else { 1.0 } - } - None => -1.0, - }) - } else { - -1.0 - } -} - -// #[ic_cdk::query] -// fn get_follow_requests(account_id: String) -> Vec { -// if is_owned(account_id.clone()) { -// FOLLOW_REQUESTS.with_borrow(| request_map | { -// Some(request_map -// .iter() -// .filter(|(id, _)| id.as_str() == account_id.as_str()) -// .map(|(id, req)| (id.clone(), req.clone())) -// .collect()) -// }) -// } else { -// None -// } -// } - -#[ic_cdk::update] -fn accept_follow_request(account_id: String, target_id: String) { - if is_owned(account_id.clone()) { - let account_id_cloned = account_id.clone(); - ACCOUNTS.with_borrow_mut(|account_map| { - if let Some(acc) = account_map.get_mut(&target_id) { - if can_view(account_id_cloned.clone(), acc.id.clone()) { - if acc.private { - // create follow request - FOLLOW_REQUESTS.with_borrow_mut(|request_map| { - request_map.insert( - target_id, - FollowRequest { - requester_id: account_id.clone(), - requested_at: now(), - }, - ); - }); - } else { - acc.followers.push((account_id_cloned.clone(), now())); - } - } - } - }); - } -} - -#[ic_cdk::update] -async fn get_followers( - storage_canister_id: Principal, - account_id: String, - target_id: String, -) -> Option> { - if can_view(account_id.clone(), target_id.clone()) { - let followers_info = ACCOUNTS.with_borrow(|account_map: &HashMap| { - account_map.get(&target_id).map(|acc: &Account| { - acc.followers - .iter() - .filter_map(|(fol, _)| account_map.get(fol).cloned()) - .collect::>() - }) - }); - - if let Some(accounts) = followers_info { - let mut result = Vec::new(); - for acc in accounts { - let mut profile_picture = None; - - if let Some(pfp) = acc.profile.profile_picture.clone() { - profile_picture = get_profile_picture(storage_canister_id, pfp).await; - } - - result.push(AccountVisibleInformation { - id: acc.id.clone(), - username: acc.profile.username.clone(), - profile_picture, - }); - } - Some(result) - } else { - None - } - } else { - None - } -} - -#[ic_cdk::update] -async fn get_following( - storage_canister_id: Principal, - account_id: String, - target_id: String, -) -> Option> { - if can_view(account_id.clone(), target_id.clone()) { - let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { - account_map.get(&target_id).map(|acc: &Account| { - acc.following - .iter() - .filter_map(|(fol, _)| account_map.get(fol).cloned()) - .collect::>() - }) - }); - - if let Some(accounts) = accounts { - let mut result = Vec::new(); - for acc in accounts { - let mut profile_picture = None; - - if let Some(pfp) = acc.profile.profile_picture.clone() { - profile_picture = get_profile_picture(storage_canister_id, pfp).await; - } - - result.push(AccountVisibleInformation { - id: acc.id.clone(), - username: acc.profile.username.clone(), - profile_picture, - }); - } - Some(result) - } else { - None - } - } else { - None - } -} - -// Posts -#[ic_cdk::update] -fn create_post(account_id: String, post: Post) { - if is_owned(account_id.clone()) { - let account_id_cloned = account_id.clone(); - let post_cloned = post.clone(); - - POSTS.with_borrow_mut(|post_map: &mut HashMap| { - post_map.insert(account_id_cloned.clone(), post_cloned.clone()); - }); - - ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { - if let Some(acc) = account_map.get_mut(&account_id_cloned) { - acc.posts.push(post_cloned.id); - } - }); - } -} - -#[ic_cdk::query] -fn get_posts(account_id: String) -> Vec { - ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.get(&account_id) { - Some(acc) => { - if can_view(account_id.clone(), acc.id.clone()) { - POSTS.with_borrow(|post_map: &HashMap| { - post_map - .values() - .filter(|p| acc.posts.contains(&p.id)) - .cloned() - .collect::>() - }) - } else { - Vec::new() - } - } - None => Vec::new(), - } - }) -} - -#[ic_cdk::update] -async fn get_feeds( - account_id: String, - page: usize, - storage_canister_id: Principal, -) -> PaginatorResponse { - // First, collect the posts that are viewable by the account - let posts: Vec = ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.get(&account_id) { - Some(acc) => { - if can_view(account_id.clone(), acc.id.clone()) { - POSTS.with_borrow(|post_map: &HashMap| { - post_map - .values() - .filter(|p| can_view(account_id.clone(), p.poster_id.clone())) - .cloned() - .collect::>() - }) - } else { - Vec::new() - } - } - None => Vec::new(), - } - }); - - // Now, for each post, fetch the poster's visible information asynchronously - let mut payloads = Vec::new(); - for post in posts { - let poster_info = - get_account_visible_information(storage_canister_id, post.poster_id.clone()).await; - let poster_info = match poster_info { - Some(info) => info, - None => AccountVisibleInformation { - id: post.poster_id.clone(), - username: String::from("Unknown"), - profile_picture: None, - }, - }; - - let post_medias = get_files(storage_canister_id, post.medias).await; - - payloads.push(FeedPost { - id: post.id, - poster: poster_info, - title: post.title, - caption: post.caption, - medias: post_medias, - likes: post.likes, - shares: post.shares, - comments: post.comments, - created_at: post.created_at, - updated_at: post.updated_at, - }); - } - - Paginator::new(payloads, vec![]).get(page, 5) -} - -#[ic_cdk::update] -fn like_post(account_id: String, post_id: String) { - POSTS.with_borrow_mut(|post_map: &mut HashMap| { - if let Some(post) = post_map.get_mut(&post_id) { - if can_view(account_id.clone(), post.poster_id.clone()) { - if post.likes.contains(&account_id) { - post.likes.retain(|p| p != &account_id); - } else { - post.likes.push(account_id.clone()); - } - } - } - }); -} - -#[ic_cdk::update] -fn comment_post(account_id: String, post_id: String, comment: Comment) { - POSTS.with_borrow_mut(|post_map: &mut HashMap| { - if let Some(post) = post_map.get_mut(&post_id) { - if can_view(account_id.clone(), post.poster_id.clone()) { - post.comments.push(comment.clone()); - } - } - }); -} - -#[ic_cdk::update] -fn remove_comment(account_id: String, post_id: String, comment_id: String) { - POSTS.with_borrow_mut(|post_map: &mut HashMap| { - if let Some(post) = post_map.get_mut(&post_id) { - if is_comment_owner(account_id.clone(), comment_id.clone()) - || is_post_owner(post.id.clone()) - { - post.comments.retain(|c| c.id != comment_id); - } - } - }); - - COMMENTS.with_borrow_mut(|comment_map: &mut HashMap| { - if is_comment_owner(account_id, comment_id.clone()) { - comment_map.retain(|id, _| *id != comment_id); - } - }); -} - -// Echo -#[ic_cdk::update] -fn post_echo(account_id: String, echo: Echo) { - let id: String = generate_uuid(); - - if is_owned(account_id.clone()) { - let id_clone = id.clone(); - ECHOS.with_borrow_mut(|echo_map: &mut HashMap| { - echo_map.insert(id, echo); - }); - - ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { - if let Some(acc) = account_map.get_mut(&account_id) { - acc.echos.push(id_clone); - } - }); - } -} - -#[derive(CandidType, Clone, Serialize, Deserialize)] -struct EchoBriefInformation { - account: AccountVisibleInformation, - echos: Vec, - seen: bool, -} - -#[ic_cdk::update] -async fn get_echos( - storage_canister_id: Principal, - account_id: String, -) -> Option> { - let mut accs: Vec = vec![]; - if is_owned(account_id.clone()) { - let get_all = ACCOUNTS.with_borrow(|account_map: &HashMap| { - match account_map.get(&account_id) { - Some(acc) => { - accs = account_map - .iter() - .filter(|(id, _)| acc.following.iter().any(|(f_id, _)| f_id == *id)) - .map(|(_, account)| account.clone()) - .collect::>(); - - let futures = accs.iter().map(|a: &Account| { - let echo_ids = a.echos.clone(); - - let seen = echo_ids.iter().all(|echo_id| { - ECHOS.with_borrow(|echo_map: &HashMap| { - echo_map.get(echo_id).is_some_and(|echo| { - echo.seen_by.iter().any(|(acc_id, _)| acc_id == &account_id) - }) - }) - }); - - let storage_canister_id = storage_canister_id; - let a_id = a.id.clone(); - - async move { - let account_info = - get_account_visible_information(storage_canister_id, a_id) - .await - .unwrap_or(AccountVisibleInformation { - id: a.id.clone(), - username: a.profile.username.clone(), - profile_picture: None, - }); - - EchoBriefInformation { - echos: echo_ids, - account: account_info, - seen, - } - } - }); - - Some(join_all(futures)) - } - None => None, - } - }); - - if let Some(call) = get_all { - Some(call.await) - } else { - None - } - } else { - None - } -} - -#[ic_cdk::query] -fn get_echo() {} - -// #[ic_cdk::update] -// async fn seeder(storage_canister_id: Principal) -> String { - -// let demo_accounts = vec![ -// ("alice", "Welcome to Alice's adventures!"), -// ("bob", "Bob's journey into decentralized land"), -// ("carol", "Carol's creative corner"), -// ]; - -// let mut created_account_ids = Vec::new(); - -// for (username, about) in &demo_accounts { -// let payload = AccountCreationPayload { -// profile: AccountProfileCreationPayload { -// username: username.to_string(), -// about: about.to_string(), -// profile_picture: None, -// }, -// private: false, -// }; -// let account_id = create_account(payload, storage_canister_id).await; -// created_account_ids.push(account_id); -// } - -// // Media templates to simulate different file types -// let media_templates = vec![ -// ("image/jpeg", "sample_image.jpg"), -// ("video/mp4", "sample_video.mp4"), -// ("application/pdf", "sample_doc.pdf"), -// ("image/png", "sample_graphic.png"), -// ]; - -// // Generate posts with unique content per user -// for ((username, caption_text, file_name), account_id) in demo_accounts.into_iter().zip(created_account_ids.iter()) { -// let post_id = generate_uuid(); - -// let medias: Vec = (0..8) -// .map(|_| { - -// StoredFile { -// id: generate_uuid(), -// name: template_name.to_string(), -// mime_type: mime_type.to_string(), -// size: fake_data.len(), -// data: fake_data, -// owner: *account_id, -// groups: vec![], -// allowed_users: vec![], -// public: true, -// uploaded_at: now(), -// } -// }) -// .collect(); - -// let post = Post { -// id: post_id.clone(), -// poster_id: account_id.clone(), -// title: format!("{}'s First Post", username), -// caption: caption_text.to_string(), -// medias, -// likes: vec![], -// shares: vec![], -// comments: vec![], -// created_at: now(), -// updated_at: now(), -// }; - -// create_post(account_id.clone(), post); -// } - -// "Seeded demo accounts with personalized posts and varied media.".to_string() -// } - -export_candid!(); +use candid::{CandidType, Principal}; +use futures::future::join_all; +use ic_cdk::{api::msg_caller, export_candid}; +use paginator::{HasFields, Paginator, PaginatorResponse}; +use serde::{Deserialize, Serialize}; +use std::{cell::RefCell, collections::HashMap}; + +use utilities::{StoredFile, generate_uuid, get_files, now, upload_files}; + +#[derive(CandidType, Clone, Serialize, Deserialize, Debug)] +struct Comment { + id: String, + comment: String, + post_id: String, + poster_id: String, + replied_to: Option, + created_at: String, + updated_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct Post { + id: String, + poster_id: String, + title: String, + caption: String, + medias: Vec, + likes: Vec, + shares: Vec, + comments: Vec, + created_at: String, + updated_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct PostCreationPayload { + id: String, + poster_id: String, + title: String, + caption: String, + medias: Vec, + likes: Vec, + shares: Vec, + comments: Vec, + created_at: String, + updated_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct FeedPost { + id: String, + poster: AccountVisibleInformation, + title: String, + caption: String, + medias: Vec, + likes: Vec, + shares: Vec, + comments: Vec, + created_at: String, + updated_at: String, +} + +impl HasFields for FeedPost { + fn get_field(&self, field_name: &str) -> String { + match field_name { + "id" => self.id.clone(), + "poster" => self.poster.username.clone(), + "title" => self.title.clone(), + "caption" => self.caption.clone(), + "medias" => format!("{:?}", self.medias), + "likes" => format!("{:?}", self.likes), + "shares" => format!("{:?}", self.shares), + "comments" => format!("{:?}", self.comments), + "created_at" => self.created_at.clone(), + "updated_at" => self.updated_at.clone(), + _ => "".to_string(), + } + } +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountProfile { + username: String, + profile_picture: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct Account { + id: String, + user_id: Principal, + profile: AccountProfile, + followers: Vec<(String, String)>, + following: Vec<(String, String)>, + posts: Vec, + echos: Vec, + blocked: Vec<(String, String)>, + private: bool, + deleted_at: Option, + created_at: String, + updated_at: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct UserAccountProfile { + username: String, + profile_picture: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct UserAccount { + id: String, + user_id: Principal, + profile: UserAccountProfile, + followers: Vec<(String, String)>, + following: Vec<(String, String)>, + posts: Vec, + echos: Vec, + blocked: Vec<(String, String)>, + private: bool, + deleted_at: Option, + created_at: String, + updated_at: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountProfileCreationPayload { + username: String, + about: String, + profile_picture: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountCreationPayload { + profile: AccountProfileCreationPayload, + private: bool, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountDetails { + account: Account, + owned: bool, + posts: Option>, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountVisibleInformation { + id: String, + username: String, + profile_picture: Option, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct AccountDeletionPayload { + account_id: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct Echo { + id: String, + account_id: String, + media: Vec, + like: usize, + share: usize, + seen_by: Vec<(String, String)>, + created_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub enum LikableType { + POST, + STORY, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct Like { + account_id: String, + likable_id: String, + likable_type: LikableType, + created_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub enum ReportType { + SPAM, + TERRORISM, + SCAM, + PROFANITY, + HATESPEECH, + RACISM, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +pub enum ReportResolveType { + ACCOUNTSUSPENDED, + ACCOUNTDELETED, + USERSUSPENDED, + USERDELETED, + WARNING, + FALSE, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct Report { + id: String, + reporter_id: String, + reported_id: String, + report_type: Vec, + created_at: String, + resolved: Vec<(ReportResolveType, Option, String)>, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct FollowRequest { + requester_id: String, + requested_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct FollowRequestReturnPayload { + requester: AccountVisibleInformation, + requested_at: String, +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct ValidityCheckingPayload { + username: String, +} + +thread_local! { + static USER_ACCOUNTS: RefCell>> = RefCell::new(HashMap::new()); + static ACCOUNTS: RefCell> = RefCell::new(HashMap::new()); + static FOLLOW_REQUESTS: RefCell> = RefCell::new(HashMap::new()); + static POSTS: RefCell> = RefCell::new(HashMap::new()); + static LIKES: RefCell> = RefCell::new(HashMap::new()); + static COMMENTS: RefCell> = RefCell::new(HashMap::new()); + static ECHOS: RefCell> = RefCell::new(HashMap::new()); + static REPORTS: RefCell> = RefCell::new(HashMap::new()); +} + +// Accounts + +fn can_view(account_id: String, target_id: String) -> bool { + ACCOUNTS.with_borrow( + |accounts: &HashMap| match accounts.get(&target_id) { + Some(acc) => { + if is_owned(target_id) { + return true; + } + + let is_blocked: bool = acc + .blocked + .iter() + .any(|(blocked_id, _)| blocked_id == &account_id); + + if is_blocked { + return false; + } + + let is_private: bool = acc.private; + + if is_private { + acc.followers + .iter() + .any(|(follower_id, _)| follower_id == &account_id) + } else { + true + } + } + None => false, + }, + ) +} + +fn is_owned(account_id: String) -> bool { + let principal = msg_caller(); + ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.get(&account_id) { + Some(acc) => acc.user_id == principal, + None => false, + } + }) +} + +fn is_post_owner(post_id: String) -> bool { + POSTS.with_borrow(|post_map| match post_map.get(&post_id) { + Some(post) => ACCOUNTS.with_borrow(|account_map| match account_map.get(&post.poster_id) { + Some(acc) => { + let principal = msg_caller(); + acc.user_id == principal + } + None => false, + }), + None => false, + }) +} + +fn is_account_owner(account_id: String) -> bool { + ACCOUNTS.with_borrow(|account_map| match account_map.get(&account_id) { + Some(acc) => { + let principal = msg_caller(); + acc.user_id == principal + } + None => false, + }) +} + +fn is_comment_owner(account_id: String, comment_id: String) -> bool { + COMMENTS.with_borrow(|comment_map| match comment_map.get(&comment_id) { + Some(c) => is_account_owner(account_id.clone()) && c.poster_id == account_id, + None => false, + }) +} + +async fn get_profile_picture( + storage_canister_id: Principal, + profile_picture_id: String, +) -> Option { + let files = get_files(storage_canister_id, vec![profile_picture_id.clone()]).await; + files.first().cloned() +} + +async fn get_account_visible_information( + storage_canister_id: Principal, + target_id: String, +) -> Option { + let account_opt = ACCOUNTS + .with_borrow(|account_map: &HashMap| account_map.get(&target_id).cloned()); + + if let Some(acc) = account_opt { + let mut profile_picture = None; + + if let Some(pfp) = acc.profile.profile_picture.clone() { + profile_picture = get_profile_picture(storage_canister_id, pfp).await; + } + + Some(AccountVisibleInformation { + id: acc.id, + username: acc.profile.username, + profile_picture, + }) + } else { + None + } +} + +#[ic_cdk::query] +fn verify_login(account_id: String) -> bool { + ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.values().find(|acc| *acc.id == account_id) { + Some(acc) => acc.user_id == msg_caller(), + None => false, + } + }) +} + +#[ic_cdk::query] +fn check_validity(payload: ValidityCheckingPayload) -> bool { + ACCOUNTS.with_borrow(|account_map: &HashMap| { + let username_is_valid: bool = account_map + .values() + .all(|acc: &Account| acc.profile.username != payload.username); + + username_is_valid + }) +} + +#[ic_cdk::update] +async fn create_account( + payload: AccountCreationPayload, + storage_canister_id: Principal, +) -> Account { + let principal: Principal = msg_caller(); + + let account_id: String = generate_uuid(); + + let mut profile_picture_id: Option = None; + + if let Some(pfp) = payload.profile.profile_picture { + let upload_response = upload_files(storage_canister_id, vec![pfp]).await; + + if !upload_response.is_empty() { + profile_picture_id = Some( + upload_response + .iter() + .map(|(id, _, _)| id.clone()) + .collect::>()[0] + .clone(), + ); + } + } + + let account_data: Account = Account { + id: account_id.clone(), + user_id: principal, + followers: Vec::new(), + following: Vec::new(), + posts: Vec::new(), + echos: Vec::new(), + blocked: Vec::new(), + profile: AccountProfile { + username: payload.profile.username.clone(), + profile_picture: profile_picture_id, + }, + private: payload.private, + deleted_at: None, + created_at: now(), + updated_at: None, + }; + + ACCOUNTS.with_borrow_mut(|accounts: &mut HashMap| { + accounts.insert(account_id.clone(), account_data.clone()); + }); + + USER_ACCOUNTS.with_borrow_mut( + |user_account_map| match user_account_map.get_mut(&principal) { + Some(user_acc) => { + user_acc.push(account_id.clone()); + } + None => { + user_account_map.insert(principal, vec![account_id.clone()]); + } + }, + ); + + account_data +} + +#[ic_cdk::update] +async fn get_account(account_id: String, storage_canister_id: Principal) -> Option { + if let Some(account) = ACCOUNTS + .with_borrow(|account_map: &HashMap| account_map.get(&account_id).cloned()) + { + let profile_picture_id = account.profile.profile_picture.clone(); + let profile_picture = match profile_picture_id { + Some(ref pfp_id) => get_profile_picture(storage_canister_id, pfp_id.clone()).await, + None => None, + }; + + Some(UserAccount { + id: account.id.clone(), + user_id: account.user_id, + profile: UserAccountProfile { + username: account.profile.username.clone(), + profile_picture, + }, + followers: account.followers.clone(), + following: account.following.clone(), + posts: account.posts.clone(), + echos: account.echos.clone(), + blocked: account.blocked.clone(), + private: account.private, + deleted_at: account.deleted_at.clone(), + created_at: account.created_at.clone(), + updated_at: account.updated_at.clone(), + }) + } else { + None + } +} + +#[ic_cdk::update] +async fn get_user_accounts(storage_canister_id: Principal) -> Vec { + let principal: Principal = msg_caller(); + + let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { + account_map + .values() + .filter(|acc: &&Account| acc.user_id == principal) + .cloned() + .collect::>() + }); + + let mut result = Vec::new(); + for acc in accounts { + let mut profile_picture = None; + + if let Some(pfp) = acc.profile.profile_picture.clone() { + profile_picture = get_profile_picture(storage_canister_id, pfp).await; + } + + result.push(AccountVisibleInformation { + id: acc.id.clone(), + username: acc.profile.username.clone(), + profile_picture, + }); + } + result +} + +#[ic_cdk::update] +fn delete_account(payload: AccountDeletionPayload) { + let account_id = payload.account_id.clone(); + + ACCOUNTS.with_borrow_mut(|accounts: &mut HashMap| { + let account = accounts.get_mut(&account_id); + + if let Some(acc) = account { + if is_owned(account_id.clone()) { + acc.deleted_at = Some(now()); + } + } + }); + + let principal: Principal = msg_caller(); + + USER_ACCOUNTS.with_borrow_mut( + |user_account_map| match user_account_map.get_mut(&principal) { + Some(user_acc) => { + user_acc.retain(|acc_id| *acc_id != account_id); + } + None => { + user_account_map.retain(|user_principal, _| user_principal != &principal); + } + }, + ); +} + +#[ic_cdk::update] +fn report_account(payload: Report) { + let mut report_data: Report = payload; + report_data.id = generate_uuid(); + + REPORTS.with_borrow_mut(|reports: &mut HashMap| { + reports.insert(report_data.id.clone(), report_data); + }); +} + +#[ic_cdk::update] +fn block_account(account_id: String, target_id: String) { + ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { + if let Some(acc) = account_map.get_mut(&account_id) { + if is_owned(account_id) { + acc.blocked.push((target_id, now())); + } + } + }) +} + +#[ic_cdk::update] +fn unblock_account(account_id: String, target_id: String) { + ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { + if let Some(acc) = account_map.get_mut(&account_id) { + if is_owned(account_id) { + acc.blocked.retain(|(blocked, _)| blocked != &target_id); + } + } + }) +} + +#[ic_cdk::query] +fn get_account_details(account_id: String, target_id: String) -> Option { + let principal: Principal = msg_caller(); + + let target_account = ACCOUNTS + .with_borrow(|accounts: &HashMap| accounts.get(&target_id).cloned()); + + match target_account { + Some(acc) => { + let owned: bool = acc.user_id == principal; + if can_view(account_id, target_id.clone()) { + let posts = POSTS.with_borrow(|post_map| { + Some( + post_map + .values() + .filter(|post| post.poster_id == target_id.clone()) + .cloned() + .collect::>(), + ) + }); + + Some(AccountDetails { + account: acc, + owned, + posts, + }) + } else { + Some(AccountDetails { + account: acc, + owned, + posts: None, + }) + } + } + None => None, + } +} + +#[ic_cdk::query] +fn get_profile(account_id: String) -> Option { + if is_owned(account_id.clone()) { + ACCOUNTS + .with_borrow(|account_map| account_map.get(&account_id).map(|acc| acc.profile.clone())) + } else { + None + } +} + +#[ic_cdk::update] +fn follow(account_id: String, target_id: String) { + if is_owned(account_id.clone()) { + let account_id_cloned = account_id.clone(); + ACCOUNTS.with_borrow_mut(|account_map| { + if let Some(acc) = account_map.get_mut(&target_id) { + if can_view(account_id_cloned.clone(), acc.id.clone()) { + if acc.private { + // create follow request + FOLLOW_REQUESTS.with_borrow_mut(|request_map| { + request_map.insert( + target_id, + FollowRequest { + requester_id: account_id.clone(), + requested_at: now(), + }, + ); + }); + } else { + acc.followers.push((account_id_cloned.clone(), now())); + } + } + } + }); + } +} + +#[ic_cdk::update] +fn unfollow(account_id: String, target_id: String) -> f32 { + if is_owned(account_id.clone()) { + ACCOUNTS.with_borrow_mut(|account_map| match account_map.get_mut(&target_id) { + Some(acc) => { + acc.followers.retain(|(a, _)| a != &account_id); + if acc.private { 0.0 } else { 1.0 } + } + None => -1.0, + }) + } else { + -1.0 + } +} + +// #[ic_cdk::query] +// fn get_follow_requests(account_id: String) -> Vec { +// if is_owned(account_id.clone()) { +// FOLLOW_REQUESTS.with_borrow(| request_map | { +// Some(request_map +// .iter() +// .filter(|(id, _)| id.as_str() == account_id.as_str()) +// .map(|(id, req)| (id.clone(), req.clone())) +// .collect()) +// }) +// } else { +// None +// } +// } + +#[ic_cdk::update] +fn accept_follow_request(account_id: String, target_id: String) { + if is_owned(account_id.clone()) { + let account_id_cloned = account_id.clone(); + ACCOUNTS.with_borrow_mut(|account_map| { + if let Some(acc) = account_map.get_mut(&target_id) { + if can_view(account_id_cloned.clone(), acc.id.clone()) { + if acc.private { + // create follow request + FOLLOW_REQUESTS.with_borrow_mut(|request_map| { + request_map.insert( + target_id, + FollowRequest { + requester_id: account_id.clone(), + requested_at: now(), + }, + ); + }); + } else { + acc.followers.push((account_id_cloned.clone(), now())); + } + } + } + }); + } +} + +#[ic_cdk::update] +async fn get_followers( + storage_canister_id: Principal, + account_id: String, + target_id: String, +) -> Option> { + if can_view(account_id.clone(), target_id.clone()) { + let followers_info = ACCOUNTS.with_borrow(|account_map: &HashMap| { + account_map.get(&target_id).map(|acc: &Account| { + acc.followers + .iter() + .filter_map(|(fol, _)| account_map.get(fol).cloned()) + .collect::>() + }) + }); + + if let Some(accounts) = followers_info { + let mut result = Vec::new(); + for acc in accounts { + let mut profile_picture = None; + + if let Some(pfp) = acc.profile.profile_picture.clone() { + profile_picture = get_profile_picture(storage_canister_id, pfp).await; + } + + result.push(AccountVisibleInformation { + id: acc.id.clone(), + username: acc.profile.username.clone(), + profile_picture, + }); + } + Some(result) + } else { + None + } + } else { + None + } +} + +#[ic_cdk::update] +async fn get_following( + storage_canister_id: Principal, + account_id: String, + target_id: String, +) -> Option> { + if can_view(account_id.clone(), target_id.clone()) { + let accounts = ACCOUNTS.with_borrow(|account_map: &HashMap| { + account_map.get(&target_id).map(|acc: &Account| { + acc.following + .iter() + .filter_map(|(fol, _)| account_map.get(fol).cloned()) + .collect::>() + }) + }); + + if let Some(accounts) = accounts { + let mut result = Vec::new(); + for acc in accounts { + let mut profile_picture = None; + + if let Some(pfp) = acc.profile.profile_picture.clone() { + profile_picture = get_profile_picture(storage_canister_id, pfp).await; + } + + result.push(AccountVisibleInformation { + id: acc.id.clone(), + username: acc.profile.username.clone(), + profile_picture, + }); + } + Some(result) + } else { + None + } + } else { + None + } +} + +// Posts +#[ic_cdk::update] +fn create_post(account_id: String, post: Post) { + if is_owned(account_id.clone()) { + let account_id_cloned = account_id.clone(); + let post_cloned = post.clone(); + + POSTS.with_borrow_mut(|post_map: &mut HashMap| { + post_map.insert(account_id_cloned.clone(), post_cloned.clone()); + }); + + ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { + if let Some(acc) = account_map.get_mut(&account_id_cloned) { + acc.posts.push(post_cloned.id); + } + }); + } +} + +#[ic_cdk::query] +fn get_posts(account_id: String) -> Vec { + ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.get(&account_id) { + Some(acc) => { + if can_view(account_id.clone(), acc.id.clone()) { + POSTS.with_borrow(|post_map: &HashMap| { + post_map + .values() + .filter(|p| acc.posts.contains(&p.id)) + .cloned() + .collect::>() + }) + } else { + Vec::new() + } + } + None => Vec::new(), + } + }) +} + +#[ic_cdk::update] +async fn get_feeds( + account_id: String, + page: usize, + storage_canister_id: Principal, +) -> PaginatorResponse { + // First, collect the posts that are viewable by the account + let posts: Vec = ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.get(&account_id) { + Some(acc) => { + if can_view(account_id.clone(), acc.id.clone()) { + POSTS.with_borrow(|post_map: &HashMap| { + post_map + .values() + .filter(|p| can_view(account_id.clone(), p.poster_id.clone())) + .cloned() + .collect::>() + }) + } else { + Vec::new() + } + } + None => Vec::new(), + } + }); + + // Now, for each post, fetch the poster's visible information asynchronously + let mut payloads = Vec::new(); + for post in posts { + let poster_info = + get_account_visible_information(storage_canister_id, post.poster_id.clone()).await; + let poster_info = match poster_info { + Some(info) => info, + None => AccountVisibleInformation { + id: post.poster_id.clone(), + username: String::from("Unknown"), + profile_picture: None, + }, + }; + + let post_medias = get_files(storage_canister_id, post.medias).await; + + payloads.push(FeedPost { + id: post.id, + poster: poster_info, + title: post.title, + caption: post.caption, + medias: post_medias, + likes: post.likes, + shares: post.shares, + comments: post.comments, + created_at: post.created_at, + updated_at: post.updated_at, + }); + } + + Paginator::new(payloads, vec![]).get(page, 5) +} + +#[ic_cdk::update] +fn like_post(account_id: String, post_id: String) { + POSTS.with_borrow_mut(|post_map: &mut HashMap| { + if let Some(post) = post_map.get_mut(&post_id) { + if can_view(account_id.clone(), post.poster_id.clone()) { + if post.likes.contains(&account_id) { + post.likes.retain(|p| p != &account_id); + } else { + post.likes.push(account_id.clone()); + } + } + } + }); +} + +#[ic_cdk::update] +fn comment_post(account_id: String, post_id: String, comment: Comment) { + POSTS.with_borrow_mut(|post_map: &mut HashMap| { + if let Some(post) = post_map.get_mut(&post_id) { + if can_view(account_id.clone(), post.poster_id.clone()) { + post.comments.push(comment.clone()); + } + } + }); +} + +#[ic_cdk::update] +fn remove_comment(account_id: String, post_id: String, comment_id: String) { + POSTS.with_borrow_mut(|post_map: &mut HashMap| { + if let Some(post) = post_map.get_mut(&post_id) { + if is_comment_owner(account_id.clone(), comment_id.clone()) + || is_post_owner(post.id.clone()) + { + post.comments.retain(|c| c.id != comment_id); + } + } + }); + + COMMENTS.with_borrow_mut(|comment_map: &mut HashMap| { + if is_comment_owner(account_id, comment_id.clone()) { + comment_map.retain(|id, _| *id != comment_id); + } + }); +} + +// Echo +#[ic_cdk::update] +fn post_echo(account_id: String, echo: Echo) { + let id: String = generate_uuid(); + + if is_owned(account_id.clone()) { + let id_clone = id.clone(); + ECHOS.with_borrow_mut(|echo_map: &mut HashMap| { + echo_map.insert(id, echo); + }); + + ACCOUNTS.with_borrow_mut(|account_map: &mut HashMap| { + if let Some(acc) = account_map.get_mut(&account_id) { + acc.echos.push(id_clone); + } + }); + } +} + +#[derive(CandidType, Clone, Serialize, Deserialize)] +struct EchoBriefInformation { + account: AccountVisibleInformation, + echos: Vec, + seen: bool, +} + +#[ic_cdk::update] +async fn get_echos( + storage_canister_id: Principal, + account_id: String, +) -> Option> { + let mut accs: Vec = vec![]; + if is_owned(account_id.clone()) { + let get_all = ACCOUNTS.with_borrow(|account_map: &HashMap| { + match account_map.get(&account_id) { + Some(acc) => { + accs = account_map + .iter() + .filter(|(id, _)| acc.following.iter().any(|(f_id, _)| f_id == *id)) + .map(|(_, account)| account.clone()) + .collect::>(); + + let futures = accs.iter().map(|a: &Account| { + let echo_ids = a.echos.clone(); + + let seen = echo_ids.iter().all(|echo_id| { + ECHOS.with_borrow(|echo_map: &HashMap| { + echo_map.get(echo_id).is_some_and(|echo| { + echo.seen_by.iter().any(|(acc_id, _)| acc_id == &account_id) + }) + }) + }); + + let storage_canister_id = storage_canister_id; + let a_id = a.id.clone(); + + async move { + let account_info = + get_account_visible_information(storage_canister_id, a_id) + .await + .unwrap_or(AccountVisibleInformation { + id: a.id.clone(), + username: a.profile.username.clone(), + profile_picture: None, + }); + + EchoBriefInformation { + echos: echo_ids, + account: account_info, + seen, + } + } + }); + + Some(join_all(futures)) + } + None => None, + } + }); + + if let Some(call) = get_all { + Some(call.await) + } else { + None + } + } else { + None + } +} + +#[ic_cdk::query] +fn get_echo() {} + +// #[ic_cdk::update] +// async fn seeder(storage_canister_id: Principal) -> String { + +// let demo_accounts = vec![ +// ("alice", "Welcome to Alice's adventures!"), +// ("bob", "Bob's journey into decentralized land"), +// ("carol", "Carol's creative corner"), +// ]; + +// let mut created_account_ids = Vec::new(); + +// for (username, about) in &demo_accounts { +// let payload = AccountCreationPayload { +// profile: AccountProfileCreationPayload { +// username: username.to_string(), +// about: about.to_string(), +// profile_picture: None, +// }, +// private: false, +// }; +// let account_id = create_account(payload, storage_canister_id).await; +// created_account_ids.push(account_id); +// } + +// // Media templates to simulate different file types +// let media_templates = vec![ +// ("image/jpeg", "sample_image.jpg"), +// ("video/mp4", "sample_video.mp4"), +// ("application/pdf", "sample_doc.pdf"), +// ("image/png", "sample_graphic.png"), +// ]; + +// // Generate posts with unique content per user +// for ((username, caption_text, file_name), account_id) in demo_accounts.into_iter().zip(created_account_ids.iter()) { +// let post_id = generate_uuid(); + +// let medias: Vec = (0..8) +// .map(|_| { + +// StoredFile { +// id: generate_uuid(), +// name: template_name.to_string(), +// mime_type: mime_type.to_string(), +// size: fake_data.len(), +// data: fake_data, +// owner: *account_id, +// groups: vec![], +// allowed_users: vec![], +// public: true, +// uploaded_at: now(), +// } +// }) +// .collect(); + +// let post = Post { +// id: post_id.clone(), +// poster_id: account_id.clone(), +// title: format!("{}'s First Post", username), +// caption: caption_text.to_string(), +// medias, +// likes: vec![], +// shares: vec![], +// comments: vec![], +// created_at: now(), +// updated_at: now(), +// }; + +// create_post(account_id.clone(), post); +// } + +// "Seeded demo accounts with personalized posts and varied media.".to_string() +// } + +export_candid!(); diff --git a/src/backend/utilities/src/lib.rs b/src/backend/utilities/src/lib.rs index e66d3d4..de7d40a 100644 --- a/src/backend/utilities/src/lib.rs +++ b/src/backend/utilities/src/lib.rs @@ -1,114 +1,114 @@ -use candid::{CandidType, Principal}; -use ic_cdk::call::Call; -use serde::{Deserialize, Serialize}; -use time::{OffsetDateTime, UtcOffset, format_description::well_known::Rfc3339}; -use uuid::Builder; - -pub fn now() -> String { - let nanos = ic_cdk::api::time(); - - OffsetDateTime::from_unix_timestamp_nanos(nanos as i128) - .expect("Failed to format time") - .to_string() -} - -pub fn now_as_datetime() -> OffsetDateTime { - let nanos = ic_cdk::api::time(); - - OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).expect("Failed to format time") -} - -pub fn custom_time_string() -> String { - let fmt = - time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); - now_as_datetime().format(&fmt).unwrap() -} - -pub fn convert_to_utc(time_string: String) -> String { - let dt = OffsetDateTime::parse(&time_string, &Rfc3339).expect("Failed to parse time string"); - - dt.to_offset(UtcOffset::UTC).format(&Rfc3339).unwrap() -} - -pub fn convert_to_tz(time_string: String, offset: f32) -> String { - let dt = OffsetDateTime::parse(&time_string, &Rfc3339).expect("Failed to parse time string"); - - let total_seconds = (offset * 3600.00) as i32; - let custom_offset = UtcOffset::from_whole_seconds(total_seconds).expect("Invalid offset"); - - dt.to_offset(custom_offset).format(&Rfc3339).unwrap() -} - -pub fn generate_uuid() -> String { - let timestamp = now_as_datetime().unix_timestamp_nanos(); - let bytes = timestamp.to_be_bytes(); - - let mut buf = [0u8; 16]; - buf[..16].copy_from_slice(&bytes[..16]); - - let builder = Builder::from_slice(&buf).expect("Error creating UUID builder."); - builder.into_uuid().to_string() -} - -#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] -pub struct Group { - id: String, - name: String, - members: Vec<(Principal, Access)>, - owner: Principal, - public: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] -pub enum Access { - Owner, // for checking purpose only, not to be used as a substitute for the owner field. - Admin, - Write, - Read, - Delete, - Removed, - Public, -} - -#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] -pub struct StoredFile { - pub id: String, - pub name: String, - pub mime_type: String, - pub size: usize, - pub data: Vec, - pub owner: Principal, - pub groups: Vec, - pub allowed_users: Vec<(Principal, Access)>, - pub public: bool, - pub uploaded_at: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] -pub enum FileUploadResolveType { - NotAuthorized, - SuccessfullyUploaded, - FailedToUpload, - AlreadyUploaded, -} - -pub async fn upload_files( - storage_canister_id: Principal, - files: Vec, -) -> Vec<(String, FileUploadResolveType, String)> { - Call::unbounded_wait(storage_canister_id, "upload_files") - .with_arg(&files) - .await - .expect("Failed to upload files.") - .candid::>() - .expect("Candid decoding failed.") -} - -pub async fn get_files(storage_canister_id: Principal, files_ids: Vec) -> Vec { - Call::unbounded_wait(storage_canister_id, "get_files_by_id") - .with_arg(&files_ids) - .await - .expect("Failed to upload files.") - .candid::>() - .expect("Candid decoding failed.") -} +use candid::{CandidType, Principal}; +use ic_cdk::call::Call; +use serde::{Deserialize, Serialize}; +use time::{OffsetDateTime, UtcOffset, format_description::well_known::Rfc3339}; +use uuid::Builder; + +pub fn now() -> String { + let nanos = ic_cdk::api::time(); + + OffsetDateTime::from_unix_timestamp_nanos(nanos as i128) + .expect("Failed to format time") + .to_string() +} + +pub fn now_as_datetime() -> OffsetDateTime { + let nanos = ic_cdk::api::time(); + + OffsetDateTime::from_unix_timestamp_nanos(nanos as i128).expect("Failed to format time") +} + +pub fn custom_time_string() -> String { + let fmt = + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]").unwrap(); + now_as_datetime().format(&fmt).unwrap() +} + +pub fn convert_to_utc(time_string: String) -> String { + let dt = OffsetDateTime::parse(&time_string, &Rfc3339).expect("Failed to parse time string"); + + dt.to_offset(UtcOffset::UTC).format(&Rfc3339).unwrap() +} + +pub fn convert_to_tz(time_string: String, offset: f32) -> String { + let dt = OffsetDateTime::parse(&time_string, &Rfc3339).expect("Failed to parse time string"); + + let total_seconds = (offset * 3600.00) as i32; + let custom_offset = UtcOffset::from_whole_seconds(total_seconds).expect("Invalid offset"); + + dt.to_offset(custom_offset).format(&Rfc3339).unwrap() +} + +pub fn generate_uuid() -> String { + let timestamp = now_as_datetime().unix_timestamp_nanos(); + let bytes = timestamp.to_be_bytes(); + + let mut buf = [0u8; 16]; + buf[..16].copy_from_slice(&bytes[..16]); + + let builder = Builder::from_slice(&buf).expect("Error creating UUID builder."); + builder.into_uuid().to_string() +} + +#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] +pub struct Group { + id: String, + name: String, + members: Vec<(Principal, Access)>, + owner: Principal, + public: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub enum Access { + Owner, // for checking purpose only, not to be used as a substitute for the owner field. + Admin, + Write, + Read, + Delete, + Removed, + Public, +} + +#[derive(Clone, Serialize, Deserialize, CandidType, Debug)] +pub struct StoredFile { + pub id: String, + pub name: String, + pub mime_type: String, + pub size: usize, + pub data: Vec, + pub owner: Principal, + pub groups: Vec, + pub allowed_users: Vec<(Principal, Access)>, + pub public: bool, + pub uploaded_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub enum FileUploadResolveType { + NotAuthorized, + SuccessfullyUploaded, + FailedToUpload, + AlreadyUploaded, +} + +pub async fn upload_files( + storage_canister_id: Principal, + files: Vec, +) -> Vec<(String, FileUploadResolveType, String)> { + Call::unbounded_wait(storage_canister_id, "upload_files") + .with_arg(&files) + .await + .expect("Failed to upload files.") + .candid::>() + .expect("Candid decoding failed.") +} + +pub async fn get_files(storage_canister_id: Principal, files_ids: Vec) -> Vec { + Call::unbounded_wait(storage_canister_id, "get_files_by_id") + .with_arg(&files_ids) + .await + .expect("Failed to upload files.") + .candid::>() + .expect("Candid decoding failed.") +} diff --git a/src/cry-token/.bash.env.template b/src/cry-token/.bash.env.template deleted file mode 100644 index 05aa7dd..0000000 --- a/src/cry-token/.bash.env.template +++ /dev/null @@ -1,26 +0,0 @@ - -# DFX CANISTER ENVIRONMENT VARIABLES -DFX_VERSION='0.27.0' -DFX_NETWORK='local' -CANISTER_ID_ICRC1_LEDGER_CANISTER='uxrrr-q7777-77774-qaaaq-cai' -CANISTER_ID='uxrrr-q7777-77774-qaaaq-cai' -CANISTER_CANDID_PATH='/mnt/c/Users/jason/Documents/programming-project/Fullstack/ICP/CRY-Token/.dfx/local/canisters/icrc1_ledger_canister/icrc1_ledger_canister.did' -# END DFX CANISTER ENVIRONMENT VARIABLES - -# DFX TOKEN VARIABLES -MINTER_ACCOUNT_ID="YOUR MINTING PRINCIPAL ID" -TOKEN_NAME="CRY Token" -TOKEN_SYMBOL="CRY" -DEPLOY_ID="TOKEN'S ACCOUNT PRINCIPAL ID" -PRE_MINTED_TOKENS=1_000_000 -TRANSFER_FEE=1 -ARCHIVE_CONTROLLER="ARCHIVE CONTROLLER PRINCIPAL ID" -TRIGGER_THRESHOLD=2000 -NUM_OF_BLOCK_TO_ARCHIVE=1000 -CYCLE_FOR_ARCHIVE_CREATION=10000000000000 -FEATURE_FLAGS=true - -# Local development args -IS_LOCAL_DEV=true -DEV_ACCOUNT_ID="YOUR LOCAL TESTING/DEVELOPMENT ACCOUNT PRINCIPAL ID" -PRESET_CANISTER_PRINCIPAL="mxzaz-hqaaa-aaaar-qaada-cai" \ No newline at end of file diff --git a/src/cry-token/.gitignore b/src/cry-token/.gitignore deleted file mode 100644 index 849fe9d..0000000 --- a/src/cry-token/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Various IDEs and Editors -.vscode/ -.idea/ -**/*~ - -# Mac OSX temporary files -.DS_Store -**/.DS_Store - -# dfx temporary files -.dfx/ - -# generated files -**/declarations/ - -# rust -target/ - -# frontend code -node_modules/ -dist/ -.svelte-kit/ - -# environment variables -.env -.bash.env - -# log file -*.log \ No newline at end of file diff --git a/src/cry-token/README.md b/src/cry-token/README.md deleted file mode 100644 index fb05da7..0000000 --- a/src/cry-token/README.md +++ /dev/null @@ -1,310 +0,0 @@ -
- -# ๐Ÿ’Ž CRY Token Ledger - -### _ICRC-1 Token Infrastructure for the Overworked Metaverse_ - -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Internet Computer](https://img.shields.io/badge/Internet%20Computer-Protocol-blue.svg)](https://internetcomputer.org/) -[![ICRC-1](https://img.shields.io/badge/Standard-ICRC--1-green.svg)](https://github.com/dfinity/ICRC-1) -[![ICRC-2](https://img.shields.io/badge/Standard-ICRC--2-green.svg)](https://github.com/dfinity/ICRC-1/tree/main/standards/ICRC-2) -[![DFX](https://img.shields.io/badge/DFX-0.27.0-blue.svg)](https://github.com/dfinity/sdk) - -**๐ŸŒ Part of the [Overworked Metaverse](https://github.com/Unchainers/Overworked) ecosystem** - -_Foundational token infrastructure for virtual economies_ ๐Ÿ’ผโœจ - -
- ---- - -## ๐ŸŽฏ Overview - -This repository contains the **ICRC-1 ledger deployment infrastructure** for the **CRY Token** - the native cryptocurrency of the [Overworked Metaverse](https://github.com/Unchainers/Overworked). - -The CRY Token powers a revolutionary virtual world where users earn real value through: - -- ๐Ÿ’ผ **Job Marketplace** - Get paid in CRY for completing virtual tasks -- ๐ŸŽ“ **Education Hubs** - Earn tokens while learning new skills -- ๐Ÿ›๏ธ **Museums & Culture** - Monetize cultural experiences and content creation -- ๐Ÿข **Virtual Offices** - Facilitate real business operations in the metaverse -- ๐Ÿ›๏ธ **Marketplace** - Trade virtual assets and services - -> **๐Ÿ“ Note**: The main token logic and smart contracts are implemented in the [Overworked Metaverse backend](https://github.com/Unchainers/Overworked). This repository focuses solely on ledger deployment and configuration. - -## โœจ What This Repository Provides - -### ๐Ÿ—๏ธ **Ledger Infrastructure** - -- **ICRC-1 Ledger Deployment** - Standard-compliant token ledger -- **ICP Ledger Integration** - Local development with ICP compatibility -- **NNS Setup** - Network Nervous System for testing environments - -### ๐Ÿ› ๏ธ **Deployment Automation** - -- **One-command deployment** - Automated setup scripts -- **Environment management** - Configurable for different networks -- **Account management** - Automated minter and controller setup - -### โšก **Standards Compliance** - -- **ICRC-1** compatible for seamless integration -- **ICRC-2** support for advanced token operations -- **Cross-ledger compatibility** with other IC tokens - -## ๐Ÿ—๏ธ Architecture - -```mermaid -graph TB - A[๏ฟฝ๏ธ CRY Token Ledger Repo] --> B[๐Ÿ“‹ ICRC-1 Ledger] - A --> C[๐Ÿ“‹ ICP Ledger - Local] - A --> D[๐Ÿ”ง Deployment Scripts] - B --> E[๐ŸŒ Overworked Backend] - C --> E - E --> F[๐Ÿ’ผ Job System] - E --> G[๐ŸŽ“ Education Hub] - E --> H[๐Ÿ›๏ธ Museums] - E --> I[๐Ÿข Virtual Offices] - - style A fill:#e1f5fe - style B fill:#e8f5e8 - style C fill:#e8f5e8 - style E fill:#f3e5f5 -``` - -## ๐Ÿš€ Quick Start - -### ๐Ÿ“‹ Prerequisites - -- **dfx** `>= 0.27.0` -- **WSL2** (for Windows users) -- **Git** - -### ๐Ÿ› ๏ธ Installation - -```bash -# Clone the repository -git clone https://github.com/Unchainers/CRY-Token.git -cd CRY-Token - -# Install dfx if not already installed -sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)" - -# Setup environment variables -cp .bash.env.template .bash.env -# Edit .bash.env with your configuration -``` - -### ๐ŸŒ Local Development - -```bash -# Start local replica -dfx start --background - -# Deploy with temporary accounts (development only) -./deploy_ledger.bash --temp-acc - -# Deploy with specific owners -./deploy_ledger.bash --set-owners - -# Deploy using current identity -./deploy_ledger.bash --with-redeem -``` - -### ๏ฟฝ Configuration Options - -| Option | Description | Usage | -| --------------- | --------------------------------- | -------------------------- | -| `--temp-acc` | ๐Ÿงช Create temporary test accounts | Development only | -| `--set-owners` | ๐Ÿ‘ฅ Set specific owner principals | `--set-owners ` | -| `--with-redeem` | ๐Ÿ”‘ Use current identity as owner | Single command setup | - -## ๐Ÿ“Š Deployed Canisters - -After successful deployment, you'll have: - -### ๐Ÿช™ **ICRC-1 Ledger Canister** - -- **Purpose**: Main CRY token ledger -- **Standard**: ICRC-1/ICRC-2 compliant -- **Functions**: Transfer, balance queries, allowances -- **Integration**: Used by Overworked backend - -### ๐Ÿ’ฐ **ICP Ledger Canister** (Local only) - -- **Purpose**: Local ICP simulation for testing -- **Standard**: ICP ledger compatible -- **Functions**: ICP transfers and balance management -- **Usage**: Development and testing only - -## ๐Ÿ’ฐ Token Configuration - -| Parameter | Value | Description | -| ---------------- | ----------- | --------------------------- | -| **Symbol** | `CRY` | Token ticker symbol | -| **Name** | `CRY Token` | Full token name | -| **Decimals** | `8` | Precision (like Bitcoin) | -| **Supply** | `1,000,000` | Initial pre-minted tokens | -| **Transfer Fee** | `0` | No fees for basic transfers | - -## ๐ŸŒŸ Integration with Overworked - -### ๐Ÿ”— **How It Connects** - -The ledger deployed by this repository is consumed by the main [Overworked Metaverse](https://github.com/Unchainers/Overworked) backend for: - -- **๐Ÿช Token Vending Machine** - ICP to CRY exchange -- **๐Ÿ”„ P2P Token Swaps** - User-to-user trading -- **๐Ÿ’ผ Job Payments** - Automated salary distribution -- **๐ŸŽ“ Education Rewards** - Learning incentives -- **๐Ÿ›๏ธ Marketplace Transactions** - Virtual asset trading - -### ๐Ÿ“‹ **Environment Variables** - -After deployment, these variables are available for the main project: - -```bash -# Auto-generated in .env file -CANISTER_ID_ICRC1_LEDGER_CANISTER= -CANISTER_ID_ICP_LEDGER_CANISTER= -DFX_NETWORK=local -``` - -## ๐Ÿ”— Usage Examples - -### **Get Canister IDs** - -```bash -# Check deployed canister IDs -dfx canister id icrc1_ledger_canister -dfx canister id icp_ledger_canister -``` - -### **Check Token Balance** - -```bash -# Use the balance checker utility -./wheretoken.bash - -# Or manually query -dfx canister call icrc1_ledger_canister icrc1_balance_of '(record { owner = principal "user-principal-here" })' -``` - -### **Integration in Main Project** - -```motoko -// In Overworked backend - reference the deployed ledger -let cryLedger : ICRC1.Service = actor("canister-id-from-env"); - -// Use the ledger for transfers -let result = await cryLedger.icrc1_transfer({ - to = { owner = userPrincipal; subaccount = null }; - amount = rewardAmount; - fee = null; - memo = null; - created_at_time = null; -}); -``` - -## ๐Ÿ“ Project Structure - -``` -CRY-Token/ -โ”œโ”€โ”€ ๐Ÿ“„ README.md # This file -โ”œโ”€โ”€ โš™๏ธ dfx.json # DFX configuration for ledgers -โ”œโ”€โ”€ ๐Ÿ”ง deploy_ledger.bash # Main deployment script -โ”œโ”€โ”€ ๐Ÿ“Š .bash.env.template # Environment template -โ”œโ”€โ”€ ๐Ÿ“Š .bash.env # Your environment config -โ”œโ”€โ”€ ๐Ÿ—‚๏ธ src/ -โ”‚ โ””โ”€โ”€ ๐Ÿ“‹ declarations/ # Generated type definitions -โ”œโ”€โ”€ ๐Ÿ“œ setup.txt # Troubleshooting guide -โ”œโ”€โ”€ ๐Ÿ” wheretoken.bash # Balance checking utility -โ””โ”€โ”€ ๐Ÿ“ *.log # Deployment logs -``` - -## ๐Ÿงช Testing & Utilities - -### ๐Ÿ” **Check Balances Across All Identities** - -```bash -# Run the balance checker -./wheretoken.bash -``` - -### ๐ŸŽฏ **Test Token Operations** - -```bash -# Check total supply -dfx canister call icrc1_ledger_canister icrc1_total_supply - -# Check token metadata -dfx canister call icrc1_ledger_canister icrc1_metadata -``` - -### ๐Ÿ“Š **Monitor Deployment** - -```bash -# Check deployment logs -tail -f nns_install.log -tail -f bishop.log -``` - -## ๐Ÿš€ Deployment - -### ๐Ÿ  **Local Testing** - -```bash -# Use local replica -export DFX_NETWORK=local -./deploy_ledger.bash --temp-acc -``` - -## โš ๏ธ Important Notes - -### ๏ฟฝ **Security Considerations** - -- Never use `--temp-acc` for mainnet deployment -- Keep your principal keys secure -- Use proper identity management for production - -### ๏ฟฝ๏ธ **Development Workflow** - -1. Deploy ledger infrastructure (this repo) -2. Copy canister IDs to main project -3. Integrate with Overworked backend -4. Test end-to-end functionality - -### ๐Ÿ“ **Troubleshooting** - -Common issues and solutions are documented in [`setup.txt`](setup.txt) - -## ๐Ÿค Contributing - -We welcome contributions to the CRY Token ledger infrastructure! - -### ๐Ÿ› ๏ธ **Development Workflow** - -1. ๐Ÿด Fork the repository -2. ๐ŸŒฟ Create a feature branch (`git checkout -b feature/ledger-improvement`) -3. โœ๏ธ Commit your changes (`git commit -m 'Improve ledger deployment'`) -4. ๐Ÿ“ค Push to the branch (`git push origin feature/ledger-improvement`) -5. ๏ฟฝ Open a Pull Request - -### ๐Ÿ“ **Code Standards** - -- Follow bash scripting best practices -- Test deployment scripts thoroughly -- Document environment variable changes -- Use meaningful commit messages - -## ๐Ÿ“ž Support & Community - -- ๐Ÿ› **Issues**: [GitHub Issues](https://github.com/Unchainers/CRY-Token/issues) -- ๐Ÿ’ฌ **Discussions**: [GitHub Discussions](https://github.com/Unchainers/CRY-Token/discussions) -- ๐ŸŒ **Main Project**: [Overworked Metaverse](https://github.com/Unchainers/Overworked) - -## ๐Ÿ™ Acknowledgments - -- ๐Ÿ—๏ธ **Internet Computer Foundation** - For the incredible ICP infrastructure -- ๐Ÿ”ง **DFINITY** - For the ICRC standards and development tools -- ๐ŸŒŸ **IC Community** - For continuous support and feedback diff --git a/src/cry-token/dfx.json b/src/cry-token/dfx.json deleted file mode 100644 index 804e3c8..0000000 --- a/src/cry-token/dfx.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "canisters": { - "icrc1_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/e915efecc8af90993ccfc499721ebe826aadba60/rs/ledger_suite/icrc1/ledger/ledger.did", - "wasm": "https://download.dfinity.systems/ic/e915efecc8af90993ccfc499721ebe826aadba60/canisters/ic-icrc1-ledger.wasm.gz" - }, - "icp_ledger_canister": { - "type": "custom", - "candid": "https://raw.githubusercontent.com/dfinity/ic/e915efecc8af90993ccfc499721ebe826aadba60/rs/ledger_suite/icp/ledger.did", - "wasm": "https://download.dfinity.systems/ic/e915efecc8af90993ccfc499721ebe826aadba60/canisters/ledger-canister.wasm.gz", - "remote": { - "id": { - "ic": "ryjl3-tyaaa-aaaaa-aaaba-cai" - } - } - } - }, - "defaults": { - "build": { - "args": "", - "packtool": "" - } - }, - "output_env_file": ".env", - "version": 1 -} diff --git a/src/cry-token/setup.txt b/src/cry-token/setup.txt deleted file mode 100644 index c156247..0000000 --- a/src/cry-token/setup.txt +++ /dev/null @@ -1,32 +0,0 @@ - - -problem 1 : Failed loading PEM file -``` -dfx failed when called with args: ["canister", "--network", "local", "call", "--argument-file", "/tmp/.tmpdoZlHO", "qaa6y-5yaaa-aaaaa-aaafa-cai", "add_wasm"], error: Error: Failed to create AgentEnvironment. -Caused by: Failed to create AgentEnvironment for network 'local'. -Caused by: Failed to load identity -Caused by: Failed to instantiate identity -Caused by: Failed to load PEM -Caused by: Failed to load PEM file from file -Caused by: Failed to decrypt PEM file at /home/jason/.config/dfx/identity/jason/identity.pem.encrypted -Caused by: Failed to read user input -Caused by: IO error: not a terminal -Caused by: not a terminal -``` - -solution - make a new identity with --storage-mode set to keyring or plaintext and use that identity before running bash script - - -problem 2 : failed write to .env permission -``` -Error: Failed while trying to deploy canisters. -Caused by: Failed to build all canisters. -Caused by: Failed while trying to build all canisters. -Caused by: The build step failed for canister 'ryjl3-tyaaa-aaaaa-aaaba-cai' (icp_ledger_canister) -Caused by: Failed to build custom canister icp_ledger_canister. -Caused by: failed to write to /mnt/c/Users/jason/Documents/programming-project/Fullstack/ICP/CRY-Token/.env -Caused by: Permission denied (os error 13) -``` - -solution - copy project into wsl directory authority instead of /mnt inside c: disk - diff --git a/src/cry-token/src/CRY-Token-backend/main.mo b/src/cry-token/src/CRY-Token-backend/main.mo deleted file mode 100644 index ad5b570..0000000 --- a/src/cry-token/src/CRY-Token-backend/main.mo +++ /dev/null @@ -1,364 +0,0 @@ -/** - * ===================================================================================== - * ICRC-2 Token Swap & Vending Machine Canister - * ===================================================================================== - * - * This canister has two primary functions: - * - * 1. P2P SWAP (Peer-to-Peer): Facilitates the decentralized swapping of two - * distinct ICRC-1/2 compliant tokens (e.g., Token A and Token B) between - * two users. - * - * 2. ICP VENDING MACHINE: Sells a specific token (Cry Token) to users in - * exchange for ICP at a fixed price. This is handled securely via the - * ICP ledger's notification system. - * - * WORKFLOW (ICP VENDING MACHINE): - * 1. Setup: An owner calls `configure_icp_swap` to set the ICP ledger, the - * Cry Token ledger, and the price. - * 2. Funding: The owner transfers Cry Tokens directly to this canister using the - * `fund_vending_machine` function. These tokens become the inventory for sale. - * 3. User Swap: A user sends ICP to this canister's account. They do NOT call - * a function directly. - * 4. Notification & Payout: The ICP ledger notifies this canister of the transfer. - * This canister then automatically calculates the amount of Cry Tokens owed - * and transfers them from its internal balance to the user. - * - * ===================================================================================== - */ -import Principal "mo:base/Principal"; -import Nat "mo:base/Nat"; -import Int "mo:base/Int"; -import HashMap "mo:base/HashMap"; -import Result "mo:base/Result"; -import Text "mo:base/Text"; -import Option "mo:base/Option"; - -actor TokenSwap { - - // ================================================================================== - // TYPES & INTERFACES (for interacting with ICRC ledgers) - // ================================================================================== - - // Standard ICRC-1 Account record. - public type Account = { - owner : Principal; - subaccount : ?[Nat8]; - }; - - // --- ICRC-1/2 Transfer & Error Types --- - public type TransferArgs = { to : Account; fee : ?Nat; memo : ?[Nat8]; from_subaccount : ?[Nat8]; created_at_time : ?Nat64; amount : Nat; }; - public type TransferFromArgs = { to : Account; fee : ?Nat; spender_subaccount : ?[Nat8]; from : Account; memo : ?[Nat8]; created_at_time : ?Nat64; amount : Nat; }; - public type TransferError = { #BadFee : { expected_fee : Nat }; #BadBurn : { min_burn_amount : Nat }; #InsufficientFunds : { balance : Nat }; #TooOld; #CreatedInFuture : { ledger_time : Nat64 }; #TemporarilyUnavailable; #Duplicate : { duplicate_of : Nat }; #GenericError : { error_code : Nat; message : Text }; }; - public type TransferFromError = { #BadFee : { expected_fee : Nat }; #BadBurn : { min_burn_amount : Nat }; #InsufficientFunds : { balance : Nat }; #TooOld; #CreatedInFuture : { ledger_time : Nat64 }; #TemporarilyUnavailable; #Duplicate : { duplicate_of : Nat }; #GenericError : { error_code : Nat; message : Text }; #InsufficientAllowance : { allowance : Nat }; }; - - // --- Types for ICP Ledger Notification --- - public type ICPTokens = Nat; - public type ICPMemo = Nat64; - public type ICPTransfer = { from: Account; to: Account; amount: ICPTokens; fee: ICPTokens; memo: ?ICPMemo; created_at_time: ?Nat64; }; - public type Transaction = { transaction_hash: Nat; kind: Text; transfer: ?ICPTransfer; }; - - // The actor interface for a generic ICRC-2 Ledger Canister. - public type ICRC2Ledger = actor { - icrc1_transfer : (TransferArgs) -> async Result.Result; - icrc2_transfer_from : (TransferFromArgs) -> async Result.Result; - }; - - // ================================================================================== - // STATE - // ================================================================================== - - // -- State for P2P Swapping -- - private stable var token_a_principal : Principal = Principal.fromText("aaaaa-aa"); - private stable var token_b_principal : Principal = Principal.fromText("aaaaa-aa"); - private var balances : HashMap.HashMap> = HashMap.HashMap>(10, Principal.equal, Principal.hash); - - // -- State for ICP Vending Machine -- - private stable var owner : ?Principal = null; - private stable var icp_ledger : ?Principal = null; - private stable var cry_token_ledger : ?Principal = null; - private stable var cry_tokens_per_icp : ?Nat = null; // Price: how many Cry Tokens for 1 full ICP (10^8 e8s) - private stable var canister_cry_token_balance : Nat = 0; // Internal balance of CRY tokens available for sale - - // ================================================================================== - // INITIALIZATION - // ================================================================================== - - // Initializes the P2P swap functionality. The ICP vending machine is configured later. - public func init_tokens(token_a : Principal, token_b : Principal) : async Result.Result { - token_a_principal := token_a; - token_b_principal := token_b; - balances := HashMap.HashMap>(10, Principal.equal, Principal.hash); - #ok("Tokens initialized successfully") - }; - - // ================================================================================== - // ICP VENDING MACHINE FUNCTIONS - // ================================================================================== - - /** - * Configures the parameters for the ICP-to-Cry-Token swap functionality. - * Can only be called by the owner. The first caller becomes the owner. - * @param icp_canister The principal of the mainnet ICP ledger canister. - * @param cry_canister The principal of your Cry Token ledger canister. - * @param price The number of Cry Tokens to give for 1 full ICP (e.g., if 1 ICP = 100 Cry, price is 100). - */ - public shared(msg) func configure_icp_swap(icp_canister: Principal, cry_canister: Principal, price: Nat) : async Result.Result { - if (owner != null and Option.get(owner, Principal.fromText("aaaaa-aa")) != msg.caller) { - return #err("Canister already configured by another owner."); - }; - if (owner == null) { - owner := ?msg.caller; - }; - icp_ledger := ?icp_canister; - cry_token_ledger := ?cry_canister; - cry_tokens_per_icp := ?price; - return #ok("ICP swap configured successfully."); - }; - - /** - * Funds the vending machine with Cry Tokens for sale. - * The owner transfers tokens to this canister to create inventory. - * @param amount The amount of Cry Tokens to transfer to the canister for sale. - */ - public shared(msg) func fund_vending_machine(amount: Nat) : async Result.Result { - if (owner == null or Option.get(owner, Principal.fromText("aaaaa-aa")) != msg.caller) { - return #err("Only the owner can fund the vending machine."); - }; - - if (cry_token_ledger == null) { - return #err("Vending machine not configured yet."); - }; - - let cry_ledger = actor_from_principal(Option.get(cry_token_ledger, Principal.fromText("aaaaa-aa"))); - - // Transfer tokens from owner to this canister - let transfer_from_args : TransferFromArgs = { - from = { owner = msg.caller; subaccount = null }; - to = { owner = Principal.fromActor(TokenSwap); subaccount = null }; - amount = amount; - fee = null; - spender_subaccount = null; - memo = null; - created_at_time = null; - }; - - let transfer_result = await cry_ledger.icrc2_transfer_from(transfer_from_args); - - switch (transfer_result) { - case (#ok(block_index)) { - canister_cry_token_balance += amount; - return #ok("Vending machine funded with " # debug_show(amount) # " tokens. Block: " # debug_show(block_index)); - }; - case (#err(err)) { - return #err("Failed to fund vending machine: " # debug_show(err)); - }; - }; - }; - - /** - * The entry point for the ICP vending machine. - * This function is called BY THE ICP LEDGER CANISTER when this canister receives ICP. - * It automatically sends the corresponding amount of Cry Tokens back to the sender. - * @param tx The transaction notification record from the ICP ledger. - */ - public shared (_msg) func transaction_notification(tx: Transaction) : async Result.Result { - // Ensure the ICP swap functionality has been configured by the owner. - if (icp_ledger == null or cry_token_ledger == null or cry_tokens_per_icp == null) { - return #err("ICP swap functionality not configured."); - }; - - // Verify the notification is a valid transfer. - switch (tx.transfer) { - case null { - return #err("Notification was not a transfer."); - }; - case (?transfer) { - // Verify the ICP was sent TO this canister. - if (transfer.to.owner != Principal.fromActor(TokenSwap)) { - return #err("This transfer was not for this canister."); - }; - - let sender_principal = transfer.from.owner; - let icp_received_e8s = transfer.amount; - - // Safely get the configured price. - let price = Option.get(cry_tokens_per_icp, 0); - - // Calculate the amount of Cry Tokens to send. - // Assumes both ICP and Cry Token have 8 decimal places. - // Formula: (icp_e8s * price) / 10^8 - let cry_tokens_to_send = (icp_received_e8s * price) / 100_000_000; - - if (cry_tokens_to_send == 0) { - return #err("ICP amount too small to receive any tokens."); - }; - - // Check if we have enough inventory - if (canister_cry_token_balance < cry_tokens_to_send) { - return #err("Insufficient token inventory. Available: " # debug_show(canister_cry_token_balance) # " tokens, Requested: " # debug_show(cry_tokens_to_send) # " tokens."); - }; - - // Create an actor for the Cry Token ledger. - let cry_ledger_actor = actor_from_principal(Option.get(cry_token_ledger, Principal.fromText("aaaaa-aa"))); - - // Prepare the arguments to send Cry Tokens back to the user. - let payout_args : TransferArgs = { - to = { owner = sender_principal; subaccount = null }; - amount = cry_tokens_to_send; - fee = null; from_subaccount = null; memo = null; created_at_time = null; - }; - - // Perform the payout. - let payout_result = await cry_ledger_actor.icrc1_transfer(payout_args); - - switch (payout_result) { - case (#ok(block_index)) { - // Deduct from our inventory - canister_cry_token_balance -= cry_tokens_to_send; - return #ok("Swap successful. Sent " # debug_show(cry_tokens_to_send) # " tokens. Block: " # debug_show(block_index) # ". Remaining inventory: " # debug_show(canister_cry_token_balance)); - }; - case (#err(err)) { - // In a real-world scenario, you might want to implement a refund mechanism here. - return #err("Swap failed during Cry Token payout: " # debug_show(err)); - }; - }; - }; - }; - }; - - /** - * Allows the owner to withdraw unsold Cry Tokens from the vending machine. - * @param amount The amount of Cry Tokens to withdraw. - */ - public shared(msg) func withdraw_vending_inventory(amount: Nat) : async Result.Result { - if (owner == null or Option.get(owner, Principal.fromText("aaaaa-aa")) != msg.caller) { - return #err("Only the owner can withdraw from the vending machine."); - }; - - if (cry_token_ledger == null) { - return #err("Vending machine not configured yet."); - }; - - if (canister_cry_token_balance < amount) { - return #err("Insufficient inventory. Available: " # debug_show(canister_cry_token_balance)); - }; - - let cry_ledger = actor_from_principal(Option.get(cry_token_ledger, Principal.fromText("aaaaa-aa"))); - - let transfer_args : TransferArgs = { - to = { owner = msg.caller; subaccount = null }; - amount = amount; - fee = null; - from_subaccount = null; - memo = null; - created_at_time = null; - }; - - let transfer_result = await cry_ledger.icrc1_transfer(transfer_args); - - switch (transfer_result) { - case (#ok(block_index)) { - canister_cry_token_balance -= amount; - return #ok("Withdrew " # debug_show(amount) # " tokens from inventory. Block: " # debug_show(block_index)); - }; - case (#err(err)) { - return #err("Failed to withdraw from inventory: " # debug_show(err)); - }; - }; - }; - - /** - * Returns the current inventory of Cry Tokens available for sale. - */ - public query func get_vending_inventory() : async Nat { - canister_cry_token_balance - }; - - // ================================================================================== - // P2P SWAP FUNCTIONS (Unchanged) - // ================================================================================== - - public shared (msg) func deposit(token : Principal, amount : Nat) : async Result.Result { - let caller = msg.caller; - if (token != token_a_principal and token != token_b_principal) { return #err("Invalid token principal for this swap canister."); }; - let token_ledger = actor_from_principal(token); - let transfer_from_args : TransferFromArgs = { from = { owner = caller; subaccount = null }; to = { owner = Principal.fromActor(TokenSwap); subaccount = null }; amount = amount; fee = null; spender_subaccount = null; memo = null; created_at_time = null; }; - let transfer_result = await token_ledger.icrc2_transfer_from(transfer_from_args); - switch (transfer_result) { - case (#ok(block_index)) { update_balance(caller, token, amount, true); return #ok(block_index); }; - case (#err(err)) { return #err(debug_show(err)); }; - }; - }; - - public shared (_msg) func swap(user_a_principal : Principal, user_b_principal : Principal) : async Result.Result { - let user_a_balance_a = get_balance_for(user_a_principal, token_a_principal); - let user_a_balance_b = get_balance_for(user_a_principal, token_b_principal); - let user_b_balance_a = get_balance_for(user_b_principal, token_a_principal); - let user_b_balance_b = get_balance_for(user_b_principal, token_b_principal); - if (user_a_balance_a == 0 and user_a_balance_b == 0) { return #err("User A has no balance to swap."); }; - if (user_b_balance_a == 0 and user_b_balance_b == 0) { return #err("User B has no balance to swap."); }; - set_balance(user_a_principal, token_a_principal, user_b_balance_a); - set_balance(user_a_principal, token_b_principal, user_b_balance_b); - set_balance(user_b_principal, token_a_principal, user_a_balance_a); - set_balance(user_b_principal, token_b_principal, user_a_balance_b); - return #ok("Swap successful. Balances have been exchanged."); - }; - - public shared (msg) func withdraw(token : Principal, amount : Nat) : async Result.Result { - let caller = msg.caller; - let current_balance = get_balance_for(caller, token); - if (current_balance < amount) { return #err("Insufficient balance to withdraw."); }; - let token_ledger = actor_from_principal(token); - let transfer_args : TransferArgs = { to = { owner = caller; subaccount = null }; amount = amount; fee = null; from_subaccount = null; memo = null; created_at_time = null; }; - let transfer_result = await token_ledger.icrc1_transfer(transfer_args); - switch (transfer_result) { - case (#ok(block_index)) { update_balance(caller, token, amount, false); return #ok(block_index); }; - case (#err(err)) { return #err(debug_show(err)); }; - }; - }; - - // ================================================================================== - // QUERY & HELPER FUNCTIONS - // ================================================================================== - - public query func get_balance(user : Principal, token : Principal) : async Nat { - return get_balance_for(user, token); - }; - - private func get_balance_for(user : Principal, token : Principal) : Nat { - switch (balances.get(user)) { - case null { return 0; }; - case (?user_balances) { - switch (user_balances.get(token)) { - case null { return 0; }; - case (?balance) { return balance; }; - }; - }; - }; - }; - - private func set_balance(user : Principal, token : Principal, new_balance : Nat) { - let user_balances = switch (balances.get(user)) { - case null { HashMap.HashMap(2, Principal.equal, Principal.hash); }; - case (?existing) { existing; }; - }; - user_balances.put(token, new_balance); - balances.put(user, user_balances); - }; - - private func update_balance(user : Principal, token : Principal, amount : Nat, is_add : Bool) { - let current_balance = get_balance_for(user, token); - let new_balance = if (is_add) { - current_balance + amount - } else { - let result = Int.abs(current_balance - amount); - if (current_balance >= amount) result else 0 - }; - set_balance(user, token, new_balance); - }; - - private func actor_from_principal(p : Principal) : ICRC2Ledger { - return actor(Principal.toText(p)); - }; -} \ No newline at end of file diff --git a/src/cry-token/wheretoken.bash b/src/cry-token/wheretoken.bash deleted file mode 100644 index 1c10c7a..0000000 --- a/src/cry-token/wheretoken.bash +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Canister ID for the ledger (update if different) -LEDGER_CANISTER="ryjl3-tyaaa-aaaaa-aaaba-cai" - -# Get all identities -IDENTITIES=$(dfx identity list) - -echo "Account balances:" -echo "-----------------" - -for IDENTITY in $IDENTITIES; do - # Switch to the identity - dfx identity use "$IDENTITY" >/dev/null 2>&1 - - # Get the account-id (hex) - ACCOUNT_ID=$(dfx ledger account-id) - - # Convert hex account-id to vec{...} format for Motoko - VEC_FORMAT=$(python3 -c "print('vec{' + ';'.join([str(b) for b in bytes.fromhex('$ACCOUNT_ID')]) + '}')") - - # Get the balance - BALANCE=$(dfx canister call $LEDGER_CANISTER account_balance "(record { account = $VEC_FORMAT })" 2>/dev/null) - - echo "$IDENTITY ($ACCOUNT_ID): $BALANCE" -done \ No newline at end of file diff --git a/src/frontend/package.json b/src/frontend/package.json index bb70ca1..09dc5b7 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -10,7 +10,13 @@ "test": "vitest run" }, "dependencies": { + "@dfinity/agent": "^2.4.1", "@dfinity/auth-client": "^2.4.1", + "@dfinity/candid": "^2.4.1", + "@dfinity/ledger-icp": "^2.6.13", + "@dfinity/ledger-icrc": "^2.9.1", + "@dfinity/principal": "^2.4.1", + "@dfinity/utils": "^2.13.2", "@hookform/resolvers": "^5.1.1", "@peculiar/webcrypto": "^1.5.0", "@radix-ui/react-avatar": "^1.1.10", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 4c94e90..46a8726 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -35,6 +35,8 @@ import ScrollToTopButton from "./utility/ScrollToTop"; import AnimatedCursor from "react-animated-cursor"; import SplashCursor from "./components/reactbits/SplashCursor/SplashCursor"; +import CryCanisterAgentProvider from "@/providers/cry-canister-agent-provider"; + // Modules Page // World Brain @@ -169,8 +171,10 @@ export default () => { return ( - - + + + + ); diff --git a/src/frontend/src/components/Layouts/navbar.tsx b/src/frontend/src/components/Layouts/navbar.tsx index 7dd7eaf..2fae007 100644 --- a/src/frontend/src/components/Layouts/navbar.tsx +++ b/src/frontend/src/components/Layouts/navbar.tsx @@ -7,15 +7,46 @@ import { Button } from "@/components/ui/button"; import { useTheme } from "@/contexts/ThemeProvider"; import { useMobile } from "@/hooks/use-mobile"; import { useNavigate } from "react-router"; +import useCryCanister from "@/hooks/use-cry-token"; +import { useAuth } from "@/hooks/use-auth-client"; +import { Tokens } from "@/types/icrc-types"; +import { error } from "console"; export function Navbar() { const navigate = useNavigate(); const { theme, toggleTheme } = useTheme(); const [isMenuOpen, setIsMenuOpen] = useState(false); const [activeSection, setActiveSection] = useState("hero"); + const [cryBallance, setCryBallance] = useState(0n); const isMobile = useMobile(); + const { cryCanister } = useCryCanister(); + const { principal, isAuthenticated } = useAuth(); + + const getCryBallance = async () => { + try { + if (isAuthenticated) { + if (!principal) + throw new Error("Authenticated but principal isn't available."); + const bal: Tokens | undefined = await cryCanister?.balance({ + owner: principal, + }); + if (bal) { + setCryBallance(bal); + } else { + throw new Error("Authenticated, but ballance was not retrieved."); + } + } else { + console.warn("Not authenticated, cannot get ballance."); + setCryBallance(0n); + } + } catch (error) { + setCryBallance(0n); + } + }; useEffect(() => { + getCryBallance(); + const handleScroll = () => { const sections = [ "hero", diff --git a/src/frontend/src/contexts/cry-token-context.tsx b/src/frontend/src/contexts/cry-token-context.tsx new file mode 100644 index 0000000..e409c55 --- /dev/null +++ b/src/frontend/src/contexts/cry-token-context.tsx @@ -0,0 +1,36 @@ +import { createContext } from "react"; +import { HttpAgent } from "@dfinity/agent"; +import { Principal } from "@dfinity/principal"; +import { IcrcLedgerCanister } from "@dfinity/ledger-icrc"; +import type { + Account, + Tokens, + TransferArg, + TransferResult, + ApproveArgs, + ApproveResult, + AllowanceArgs, + Allowance, + TransferFromArgs, + TransferFromResult, + MetadataValue, + StandardRecord, +} from "../../../declarations/icrc1_ledger_canister/icrc1_ledger_canister.did"; + +export interface CryTokenContextType { + agent: HttpAgent | null; + tokenCanisterID: Principal | null; + icpCanisterID: Principal | null; + isLoading: boolean; + cryCanister: IcrcLedgerCanister | null; +} + +const CryTokenContext = createContext({ + agent: null, + tokenCanisterID: null, + icpCanisterID: null, + isLoading: true, + cryCanister: null, +}); + +export default CryTokenContext; diff --git a/src/frontend/src/hooks/use-cry-token.tsx b/src/frontend/src/hooks/use-cry-token.tsx new file mode 100644 index 0000000..e8640fc --- /dev/null +++ b/src/frontend/src/hooks/use-cry-token.tsx @@ -0,0 +1,12 @@ +import CryTokenContext from "@/contexts/cry-token-context"; +import { useContext } from "react"; + +export default function useCryCanister() { + const context = useContext(CryTokenContext); + + if (!context) { + throw new Error("useStorage must be used within StorageProvider."); + } + + return context; +} diff --git a/src/frontend/src/providers/cry-canister-agent-provider.tsx b/src/frontend/src/providers/cry-canister-agent-provider.tsx new file mode 100644 index 0000000..13caecd --- /dev/null +++ b/src/frontend/src/providers/cry-canister-agent-provider.tsx @@ -0,0 +1,114 @@ +import React, { useMemo, useState, useEffect } from "react"; +import { Principal } from "@dfinity/principal"; +import { HttpAgent, ActorSubclass } from "@dfinity/agent"; +import CryTokenContext from "@/contexts/cry-token-context"; +import { createActor } from "../../../declarations/icrc1_ledger_canister"; +import type { _SERVICE } from "../../../declarations/icrc1_ledger_canister/icrc1_ledger_canister.did"; +import { createAgent } from "@dfinity/utils"; +import { IcrcLedgerCanister } from "@dfinity/ledger-icrc"; +import { AuthClient } from "@dfinity/auth-client"; +import { useAuth } from "@/hooks/use-auth-client"; +import type { + Account, + Tokens, + TransferArg, + TransferResult, + ApproveArgs, + ApproveResult, + AllowanceArgs, + Allowance, + TransferFromArgs, + TransferFromResult, + MetadataValue, + StandardRecord, +} from "../../../declarations/icrc1_ledger_canister/icrc1_ledger_canister.did"; + +export default function CryCanisterAgentProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [isLoading, setIsLoading] = useState(true); + const [agent, setAgent] = useState(null); + const [tokenActor, setTokenActor] = useState | null>( + null, + ); + const [cryCanister, setCryCanister] = useState( + null, + ); + + const { identity, isAuthenticated } = useAuth(); + + const tokenCanisterID = useMemo(() => { + const id = import.meta.env.CANISTER_ID_ICRC1_LEDGER_CANISTER; + return id ? Principal.fromText(id) : null; + }, []); + + const icpCanisterID = useMemo(() => { + const id = import.meta.env.CANISTER_ID_ICP_LEDGER_CANISTER; + return id ? Principal.fromText(id) : null; + }, []); + + // Initialize agent and token actor + useEffect(() => { + async function initializeAgent() { + try { + setIsLoading(true); + + // Handle unauthenticated state gracefully + if (!isAuthenticated || !identity) { + console.log("User not authenticated, clearing agent and canister"); + setAgent(null); + setCryCanister(null); + setIsLoading(false); + return; + } + + // Check if token canister ID is available + if (!tokenCanisterID) { + console.warn("Token canister ID not available"); + setAgent(null); + setCryCanister(null); + setIsLoading(false); + return; + } + + // Create agent for the IC network + + const agent = await createAgent({ + identity, // your authenticated identity + host: + process.env.NODE_ENV === "development" + ? "http://localhost:4943" + : "https://icp0.io", // mainnet gateway + }); + + const cryCanister = IcrcLedgerCanister.create({ + agent, + canisterId: tokenCanisterID, + }); + + setIsLoading(false); + } catch (error) { + console.error("Failed to initialize agent:", error); + setIsLoading(false); + } + } + + initializeAgent(); + }, [tokenCanisterID, identity, isAuthenticated]); + + return ( + + {children} + + ); +} diff --git a/src/frontend/src/types/icrc-types.ts b/src/frontend/src/types/icrc-types.ts new file mode 100644 index 0000000..2a98ac8 --- /dev/null +++ b/src/frontend/src/types/icrc-types.ts @@ -0,0 +1,126 @@ +// icrc.ts + +// Types for ICRC-1 and ICRC-2 + +export type Subaccount = Uint8Array; // 32 bytes +export type Principal = string; // textual representation + +export interface Account { + owner: Principal; + subaccount?: Subaccount | null; +} + +export type Tokens = bigint; // Nat +export type Timestamp = bigint; // Nat64 + +export type MetadataValue = + | { Nat: Tokens } + | { Int: bigint } + | { Text: string } + | { Blob: Uint8Array }; + +export interface TransferArg { + from_subaccount?: Subaccount | null; + to: Account; + amount: Tokens; + fee?: Tokens | null; + memo?: Uint8Array | null; + created_at_time?: Timestamp | null; +} + +export type TransferError = + | { BadFee: { expected_fee: Tokens } } + | { BadBurn: { min_burn_amount: Tokens } } + | { InsufficientFunds: { balance: Tokens } } + | { TooOld: null } + | { CreatedInFuture: { ledger_time: Timestamp } } + | { Duplicate: { duplicate_of: bigint } } + | { TemporarilyUnavailable: null } + | { GenericError: { error_code: bigint; message: string } }; + +export type TransferResult = + | { Ok: bigint } // BlockIndex + | { Err: TransferError }; + +export interface StandardRecord { + name: string; + url: string; +} + +// ICRC-2 types +export interface ApproveArgs { + from_subaccount?: Subaccount | null; + spender: Account; + amount: Tokens; + expected_allowance?: Tokens | null; + expires_at?: Timestamp | null; + fee?: Tokens | null; + memo?: Uint8Array | null; + created_at_time?: Timestamp | null; +} + +export type ApproveError = + | { BadFee: { expected_fee: Tokens } } + | { InsufficientFunds: { balance: Tokens } } + | { AllowanceChanged: { current_allowance: Tokens } } + | { Expired: { ledger_time: Timestamp } } + | { TooOld: null } + | { CreatedInFuture: { ledger_time: Timestamp } } + | { Duplicate: { duplicate_of: bigint } } + | { TemporarilyUnavailable: null } + | { GenericError: { error_code: bigint; message: string } }; + +export type ApproveResult = { Ok: bigint } | { Err: ApproveError }; + +export interface AllowanceArgs { + account: Account; + spender: Account; +} + +export interface Allowance { + allowance: Tokens; + expires_at?: Timestamp | null; +} + +export interface TransferFromArgs { + spender_subaccount?: Subaccount | null; + from: Account; + to: Account; + amount: Tokens; + fee?: Tokens | null; + memo?: Uint8Array | null; + created_at_time?: Timestamp | null; +} + +export type TransferFromError = + | { BadFee: { expected_fee: Tokens } } + | { BadBurn: { min_burn_amount: Tokens } } + | { InsufficientFunds: { balance: Tokens } } + | { InsufficientAllowance: { allowance: Tokens } } + | { TooOld: null } + | { CreatedInFuture: { ledger_time: Timestamp } } + | { Duplicate: { duplicate_of: bigint } } + | { TemporarilyUnavailable: null } + | { GenericError: { error_code: bigint; message: string } }; + +export type TransferFromResult = { Ok: bigint } | { Err: TransferFromError }; + +// ICRC-1 and ICRC-2 method signatures (for use with @dfinity/agent or candid-js) +export interface ICRC1_2Service { + // ICRC-1 + icrc1_name(): Promise; + icrc1_symbol(): Promise; + icrc1_decimals(): Promise; + icrc1_metadata(): Promise<[string, MetadataValue][]>; + icrc1_total_supply(): Promise; + icrc1_fee(): Promise; + icrc1_minting_account(): Promise; + icrc1_balance_of(account: Account): Promise; + icrc1_transfer(args: TransferArg): Promise; + icrc1_supported_standards(): Promise; + + // ICRC-2 + icrc2_approve(args: ApproveArgs): Promise; + icrc2_allowance(args: AllowanceArgs): Promise; + icrc2_transfer_from(args: TransferFromArgs): Promise; +}