diff --git a/.github/workflows/deploy-preview.yml b/.github/workflows/deploy-preview.yml index f69230dd..43753fd0 100644 --- a/.github/workflows/deploy-preview.yml +++ b/.github/workflows/deploy-preview.yml @@ -51,6 +51,8 @@ jobs: flyctl secrets set GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET --app "$APP_NAME" --stage flyctl secrets set GRPC_AUTH_TOKEN="$GRPC_AUTH_TOKEN" --app "$APP_NAME" --stage flyctl secrets set HOTORNOT_GOOGLE_CLIENT_SECRET="$HOTORNOT_GOOGLE_CLIENT_SECRET" --app "$APP_NAME" --stage + flyctl secrets set ICPUMPFUN_GOOGLE_CLIENT_SECRET="$ICPUMPFUN_GOOGLE_CLIENT_SECRET" --app "$APP_NAME" --stage + flyctl secrets set "HON_GOOGLE_SERVICE_ACCOUNT=$HON_GOOGLE_SERVICE_ACCOUNT" --app "$APP_NAME" --stage flyctl deploy --app $APP_NAME env: CF_TOKEN: ${{ secrets.CLOUDFLARE_STREAM_IMAGES_ANALYTICS_READ_WRITE_SECRET }} @@ -61,3 +63,5 @@ jobs: FLY_API_TOKEN: ${{ secrets.HOT_OR_NOT_WEB_LEPTOS_SSR_FLY_IO_GITHUB_ACTION }} GRPC_AUTH_TOKEN: ${{ secrets.OFF_CHAIN_AGENT_GRPC_AUTH_TOKEN }} HOTORNOT_GOOGLE_CLIENT_SECRET: ${{ secrets.HOT_OR_NOT_WTF_DOMAIN_GOOGLE_LOGIN_AUTH_CLIENT_SECRET }} + ICPUMPFUN_GOOGLE_CLIENT_SECRET: ${{secrets.IC_PUMP_FUN_GOOGLE_AUTH_CLIENT_SECRET}} + HON_GOOGLE_SERVICE_ACCOUNT: ${{ secrets. HOT_OR_NOT_FEED_INTELLIGENCE_FIREBASE_PROJECT_EVENTS_BQ_SERVICE_ACCOUNT_JSON_FOR_WEB_LEPTOS_SSR_APP }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 07b50668..4e5a646b 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -32,6 +32,8 @@ jobs: flyctl secrets set GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET --app "hot-or-not-web-leptos-ssr-staging" --stage flyctl secrets set GRPC_AUTH_TOKEN="$GRPC_AUTH_TOKEN" --app "hot-or-not-web-leptos-ssr-staging" --stage flyctl secrets set HOTORNOT_GOOGLE_CLIENT_SECRET="$HOTORNOT_GOOGLE_CLIENT_SECRET" --app "hot-or-not-web-leptos-ssr-staging" --stage + flyctl secrets set ICPUMPFUN_GOOGLE_CLIENT_SECRET="$ICPUMPFUN_GOOGLE_CLIENT_SECRET" --app "hot-or-not-web-leptos-ssr-staging" --stage + flyctl secrets set "HON_GOOGLE_SERVICE_ACCOUNT=$HON_GOOGLE_SERVICE_ACCOUNT" --app "hot-or-not-web-leptos-ssr-staging" --stage env: CF_TOKEN: ${{ secrets.CLOUDFLARE_STREAM_IMAGES_ANALYTICS_READ_WRITE_SECRET }} BACKEND_ADMIN_IDENTITY: ${{ secrets.YRAL_WHITELISTED_BACKEND_GLOBAL_ADMIN_SECRET_KEY }} @@ -41,6 +43,8 @@ jobs: FLY_API_TOKEN: ${{ secrets.HOT_OR_NOT_WEB_LEPTOS_SSR_FLY_IO_GITHUB_ACTION }} GRPC_AUTH_TOKEN: ${{ secrets.OFF_CHAIN_AGENT_GRPC_AUTH_TOKEN }} HOTORNOT_GOOGLE_CLIENT_SECRET: ${{ secrets.HOT_OR_NOT_WTF_DOMAIN_GOOGLE_LOGIN_AUTH_CLIENT_SECRET }} + ICPUMPFUN_GOOGLE_CLIENT_SECRET: ${{secrets.IC_PUMP_FUN_GOOGLE_AUTH_CLIENT_SECRET}} + HON_GOOGLE_SERVICE_ACCOUNT: ${{ secrets. HOT_OR_NOT_FEED_INTELLIGENCE_FIREBASE_PROJECT_EVENTS_BQ_SERVICE_ACCOUNT_JSON_FOR_WEB_LEPTOS_SSR_APP }} - name: Deploy a docker container to Fly.io run: flyctl deploy --remote-only -c fly-staging.toml env: diff --git a/.github/workflows/deploy-to-production-on-merge-to-main.yaml b/.github/workflows/deploy-to-production-on-merge-to-main.yaml index b19a7a7a..bcba1422 100644 --- a/.github/workflows/deploy-to-production-on-merge-to-main.yaml +++ b/.github/workflows/deploy-to-production-on-merge-to-main.yaml @@ -32,6 +32,8 @@ jobs: flyctl secrets set GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET --app "hot-or-not-web-leptos-ssr" --stage flyctl secrets set GRPC_AUTH_TOKEN="$GRPC_AUTH_TOKEN" --app "hot-or-not-web-leptos-ssr" --stage flyctl secrets set HOTORNOT_GOOGLE_CLIENT_SECRET="$HOTORNOT_GOOGLE_CLIENT_SECRET" --app "hot-or-not-web-leptos-ssr" --stage + flyctl secrets set ICPUMPFUN_GOOGLE_CLIENT_SECRET="$ICPUMPFUN_GOOGLE_CLIENT_SECRET" --app "hot-or-not-web-leptos-ssr" --stage + flyctl secrets set "HON_GOOGLE_SERVICE_ACCOUNT=$HON_GOOGLE_SERVICE_ACCOUNT" --app "hot-or-not-web-leptos-ssr" --stage env: CF_TOKEN: ${{ secrets.CLOUDFLARE_STREAM_IMAGES_ANALYTICS_READ_WRITE_SECRET }} BACKEND_ADMIN_IDENTITY: ${{ secrets.YRAL_WHITELISTED_BACKEND_GLOBAL_ADMIN_SECRET_KEY }} @@ -41,6 +43,8 @@ jobs: FLY_API_TOKEN: ${{ secrets.HOT_OR_NOT_WEB_LEPTOS_SSR_FLY_IO_GITHUB_ACTION }} GRPC_AUTH_TOKEN: ${{ secrets.OFF_CHAIN_AGENT_GRPC_AUTH_TOKEN }} HOTORNOT_GOOGLE_CLIENT_SECRET: ${{ secrets.HOT_OR_NOT_WTF_DOMAIN_GOOGLE_LOGIN_AUTH_CLIENT_SECRET }} + ICPUMPFUN_GOOGLE_CLIENT_SECRET: ${{secrets.IC_PUMP_FUN_GOOGLE_AUTH_CLIENT_SECRET}} + HON_GOOGLE_SERVICE_ACCOUNT: ${{ secrets. HOT_OR_NOT_FEED_INTELLIGENCE_FIREBASE_PROJECT_EVENTS_BQ_SERVICE_ACCOUNT_JSON_FOR_WEB_LEPTOS_SSR_APP }} - name: Deploy a docker container to Fly.io run: flyctl deploy --remote-only -c fly-prod.toml env: diff --git a/Cargo.lock b/Cargo.lock index 8e84d247..5649ff4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aead" version = "0.5.2" @@ -52,6 +58,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -119,6 +136,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "ascii-canvas" version = "3.0.0" @@ -128,6 +151,19 @@ dependencies = [ "term", ] +[[package]] +name = "async-compression" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-lock" version = "3.4.0" @@ -147,7 +183,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -169,7 +205,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -180,7 +216,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -200,7 +236,7 @@ dependencies = [ "manyhow", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -212,11 +248,11 @@ dependencies = [ "collection_literals", "interpolator", "manyhow", - "proc-macro-utils", + "proc-macro-utils 0.8.0", "proc-macro2", "quote", "quote-use", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -255,7 +291,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -299,7 +335,7 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -314,7 +350,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -323,9 +359,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ + "futures-core", "getrandom", "instant", + "pin-project-lite", "rand", + "tokio", ] [[package]] @@ -338,7 +377,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -349,6 +388,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.1" @@ -461,6 +506,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -543,6 +600,30 @@ dependencies = [ "serde_with", ] +[[package]] +name = "borsh" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.76", + "syn_derive", +] + [[package]] name = "bstr" version = "1.10.0" @@ -560,6 +641,38 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byte-unit" +version = "4.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da78b32057b8fdfc352504708feeba7216dcd65a2c9ab02978cbd288d1279b6c" +dependencies = [ + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -592,7 +705,7 @@ version = "0.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7c8c50262271cdf5abc979a5f76515c234e764fa025d1ba4862c0f0bcda0e95" dependencies = [ - "ahash", + "ahash 0.8.11", "hashbrown 0.14.5", "instant", "once_cell", @@ -619,9 +732,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "camino" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3054fea8a20d8ff3968d5b22cc27501d2b08dc4decdb31b184323f00c5ef23bb" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" [[package]] name = "candid" @@ -655,7 +768,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -667,7 +780,7 @@ dependencies = [ "anyhow", "candid", "codespan-reporting", - "convert_case", + "convert_case 0.6.0", "hex", "lalrpop", "lalrpop-util", @@ -679,9 +792,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.13" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "shlex", ] @@ -692,6 +805,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -807,6 +926,42 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "comparable" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb513ee8037bf08c5270ecefa48da249f4c58e57a71ccfce0a5b0877d2a20eb2" +dependencies = [ + "comparable_derive", + "comparable_helper", + "pretty_assertions", + "serde", +] + +[[package]] +name = "comparable_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54b9c40054eb8999c5d1d36fdc90e4e5f7ff0d1d9621706f360b3cbc8beb828" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "comparable_helper" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5437e327e861081c91270becff184859f706e3e50f5301a9d4dc8eb50752c3" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -822,7 +977,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ - "convert_case", + "convert_case 0.6.0", "lazy_static", "nom", "pathdiff", @@ -876,6 +1031,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.6.0" @@ -1004,7 +1165,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1065,7 +1226,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1087,7 +1248,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1118,7 +1279,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1150,9 +1311,45 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", +] + +[[package]] +name = "dfn_candid" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "dfn_core", + "ic-base-types", + "on_wire", + "serde", +] + +[[package]] +name = "dfn_core" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "ic-base-types", + "on_wire", +] + +[[package]] +name = "dfn_protobuf" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "on_wire", + "prost 0.12.6", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.9.0" @@ -1224,7 +1421,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1372,7 +1569,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1384,7 +1581,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1393,6 +1590,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.9" @@ -1432,9 +1638,9 @@ checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ff" @@ -1474,6 +1680,29 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "firestore" +version = "0.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483bce3ebe3bec625c4a3800a84966e966604e274ea9acbe7e6429477f915445" +dependencies = [ + "async-trait", + "backoff", + "chrono", + "futures", + "gcloud-sdk", + "hex", + "hyper 1.4.1", + "rand", + "rsb_derive", + "rvstruct", + "serde", + "struct-path", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1482,12 +1711,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.0", ] [[package]] @@ -1520,6 +1749,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.30" @@ -1576,7 +1811,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1609,6 +1844,34 @@ dependencies = [ "slab", ] +[[package]] +name = "gcloud-sdk" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d92f38cbe5b8796d2ab3f3c5f3bc286aa778015d5c5f67e2d0cbbe5d348473f" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures", + "hyper 1.4.1", + "jsonwebtoken", + "once_cell", + "prost 0.13.1", + "prost-types", + "reqwest 0.12.7", + "secret-vault-value", + "serde", + "serde_json", + "tokio", + "tonic", + "tower 0.5.1", + "tower-layer", + "tower-util", + "tracing", + "url", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1759,9 +2022,9 @@ dependencies = [ [[package]] name = "gix-config-value" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b328997d74dd15dc71b2773b162cb4af9a25c424105e4876e6d0686ab41c383e" +checksum = "03f76169faa0dec598eac60f83d7fcdd739ec16596eca8fb144c88973dbe6f8c" dependencies = [ "bitflags 2.6.0", "bstr", @@ -1831,9 +2094,9 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6adf99c27cdf17b1c4d77680c917e0d94d8783d4e1c73d3be0d1d63107163d7a" +checksum = "f2bfe6249cfea6d0c0e0990d5226a4cb36f030444ba9e35e0639275db8f98575" dependencies = [ "fastrand", "gix-features", @@ -1842,9 +2105,9 @@ dependencies = [ [[package]] name = "gix-glob" -version = "0.16.4" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7df15afa265cc8abe92813cd354d522f1ac06b29ec6dfa163ad320575cb447" +checksum = "74908b4bbc0a0a40852737e5d7889f676f081e340d5451a16e5b4c50d592f111" dependencies = [ "bitflags 2.6.0", "bstr", @@ -1920,7 +2183,7 @@ checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -1982,9 +2245,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.9" +version = "0.10.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d23d5bbda31344d8abc8de7c075b3cf26e5873feba7c4a15d916bce67382bd9" +checksum = "38d5b8722112fa2fa87135298780bc833b0e9f6c56cc82795d209804b3a03484" dependencies = [ "bstr", "gix-trace", @@ -2071,9 +2334,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1547d26fa5693a7f34f05b4a3b59a90890972922172653bcb891ab3f09f436df" +checksum = "0fe4d52f30a737bbece5276fab5d3a8b276dc2650df963e293d0673be34e7a5f" dependencies = [ "bitflags 2.6.0", "gix-path", @@ -2083,9 +2346,9 @@ dependencies = [ [[package]] name = "gix-tempfile" -version = "14.0.1" +version = "14.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006acf5a613e0b5cf095d8e4b3f48c12a60d9062aa2b2dd105afaf8344a5600c" +checksum = "046b4927969fa816a150a0cda2e62c80016fe11fb3c3184e4dddf4e542f108aa" dependencies = [ "gix-fs", "libc", @@ -2119,9 +2382,9 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.27.4" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2eb9b35bba92ea8f0b5ab406fad3cf6b87f7929aa677ff10aa042c6da621156" +checksum = "fd280c5e84fb22e128ed2a053a0daeacb6379469be6a85e3d518a0636e160c89" dependencies = [ "bstr", "gix-features", @@ -2245,7 +2508,7 @@ dependencies = [ "gloo-utils", "http 0.2.12", "js-sys", - "pin-project", + "pin-project 1.1.5", "serde", "serde_json", "thiserror", @@ -2266,7 +2529,7 @@ dependencies = [ "gloo-utils", "http 1.1.0", "js-sys", - "pin-project", + "pin-project 1.1.5", "serde", "serde_json", "thiserror", @@ -2350,10 +2613,10 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -2362,7 +2625,7 @@ version = "0.1.0" source = "git+https://github.com/go-bazzinga/gob-cloudflare?rev=c847ba87ecc73a33520b24bd62503420d7e23e3e#c847ba87ecc73a33520b24bd62503420d7e23e3e" dependencies = [ "bytes", - "reqwest 0.12.5", + "reqwest 0.12.7", "serde", "serde_json", "thiserror", @@ -2412,9 +2675,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -2450,6 +2713,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -2463,7 +2729,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -2490,6 +2756,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hickory-proto" @@ -2589,10 +2858,11 @@ dependencies = [ "codee", "console_error_panic_hook", "console_log", - "convert_case", + "convert_case 0.6.0", "crc32fast", "dotenv", "enum_dispatch", + "firestore", "futures", "gloo", "gloo-utils", @@ -2601,8 +2871,10 @@ dependencies = [ "hmac", "http 1.1.0", "ic-agent", + "ic-base-types", "icondata", "icondata_core", + "icp-ledger", "js-sys", "k256", "leptos", @@ -2614,25 +2886,30 @@ dependencies = [ "log", "once_cell", "openidconnect", - "prost", + "priority-queue", + "prost 0.13.1", "rand_chacha", "redb", "redis", - "reqwest 0.12.5", + "reqwest 0.12.7", + "rust_decimal", "serde", "serde-wasm-bindgen", "serde_bytes", "serde_json", "simple_logger", + "sns-validation", + "speedate", "testcontainers", "thiserror", "tokio", "tonic", "tonic-build", "tonic-web-wasm-client", - "tower", + "tower 0.4.13", "tower-http", "tracing", + "urlencoding", "uts2ts", "wasm-bindgen", "wasm-bindgen-futures", @@ -2726,6 +3003,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.30" @@ -2759,7 +3042,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -2882,7 +3165,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -2948,11 +3231,11 @@ dependencies = [ "k256", "leb128", "p256", - "pem", + "pem 2.0.1", "pkcs8", "rand", "rangemap", - "reqwest 0.12.5", + "reqwest 0.12.7", "ring", "rustls-webpki 0.101.7", "sec1", @@ -2972,75 +3255,274 @@ dependencies = [ ] [[package]] -name = "ic-certification" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64ee3d8b6e81b51f245716d3e0badb63c283c00f3c9fb5d5219afc30b5bf821" +name = "ic-base-types" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" dependencies = [ - "hex", + "byte-unit", + "bytes", + "candid", + "comparable", + "ic-crypto-sha2", + "ic-protobuf", + "phantom_newtype", + "prost 0.12.6", + "serde", +] + +[[package]] +name = "ic-btc-interface" +version = "0.2.0" +source = "git+https://github.com/dfinity/bitcoin-canister?rev=62a71e47c491fb842ccc257b1c675651501f4b82#62a71e47c491fb842ccc257b1c675651501f4b82" +dependencies = [ + "candid", "serde", "serde_bytes", - "sha2 0.10.8", ] [[package]] -name = "ic-transport-types" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce14c068bd053c0f5ab525326f3f358f265cdfcae279fbf6461fb529e9682acd" +name = "ic-btc-types-internal" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" dependencies = [ "candid", - "hex", - "ic-certification", - "leb128", + "ic-btc-interface", + "ic-error-types", + "ic-protobuf", "serde", "serde_bytes", - "serde_repr", - "sha2 0.10.8", - "thiserror", ] [[package]] -name = "ic-verify-bls-signature" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583b1c03380cf86059160cc6c91dcbf56c7b5f141bf3a4f06bc79762d775fac4" +name = "ic-canister-log" +version = "0.2.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" dependencies = [ - "bls12_381", - "lazy_static", - "pairing", - "sha2 0.9.9", + "serde", ] [[package]] -name = "ic_principal" -version = "0.1.1" +name = "ic-cdk" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +checksum = "3b1da6a25b045f9da3c9459c0cb2b0700ac368ee16382975a17185a23b9c18ab" dependencies = [ - "arbitrary", - "crc32fast", - "data-encoding", + "candid", + "ic-cdk-macros", + "ic0", "serde", - "sha2 0.10.8", - "thiserror", + "serde_bytes", ] [[package]] -name = "icondata" -version = "0.3.1" +name = "ic-cdk-macros" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e75326892b8f4aa213dae3fef9214b9bb5a865a5267ac2143393b72cd78ffa" +checksum = "a45800053d80a6df839a71aaea5797e723188c0b992618208ca3b941350c7355" dependencies = [ - "icondata_ai", - "icondata_bi", - "icondata_bs", - "icondata_cg", - "icondata_ch", - "icondata_core", - "icondata_fa", - "icondata_fi", - "icondata_hi", + "candid", + "proc-macro2", + "quote", + "serde", + "serde_tokenstream", + "syn 1.0.109", +] + +[[package]] +name = "ic-certification" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64ee3d8b6e81b51f245716d3e0badb63c283c00f3c9fb5d5219afc30b5bf821" +dependencies = [ + "hex", + "serde", + "serde_bytes", + "sha2 0.10.8", +] + +[[package]] +name = "ic-constants" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" + +[[package]] +name = "ic-crypto-internal-sha2" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "sha2 0.10.8", +] + +[[package]] +name = "ic-crypto-sha2" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "ic-crypto-internal-sha2", +] + +[[package]] +name = "ic-error-types" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "ic-protobuf", + "ic-utils", + "serde", + "strum", + "strum_macros", +] + +[[package]] +name = "ic-ledger-canister-core" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "async-trait", + "candid", + "ic-base-types", + "ic-canister-log", + "ic-constants", + "ic-ledger-core", + "ic-ledger-hash-of", + "ic-management-canister-types", + "ic-utils", + "num-traits", + "serde", +] + +[[package]] +name = "ic-ledger-core" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "ic-ledger-hash-of", + "num-traits", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic-ledger-hash-of" +version = "0.1.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "hex", + "serde", +] + +[[package]] +name = "ic-management-canister-types" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "ic-base-types", + "ic-btc-interface", + "ic-btc-types-internal", + "ic-error-types", + "ic-protobuf", + "num-traits", + "serde", + "serde_bytes", + "serde_cbor", + "strum", + "strum_macros", +] + +[[package]] +name = "ic-protobuf" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "bincode", + "candid", + "erased-serde", + "maplit", + "prost 0.12.6", + "serde", + "serde_json", + "slog", +] + +[[package]] +name = "ic-transport-types" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce14c068bd053c0f5ab525326f3f358f265cdfcae279fbf6461fb529e9682acd" +dependencies = [ + "candid", + "hex", + "ic-certification", + "leb128", + "serde", + "serde_bytes", + "serde_repr", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "ic-utils" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "hex", + "prost 0.12.6", + "scoped_threadpool", + "serde", + "serde_bytes", +] + +[[package]] +name = "ic-verify-bls-signature" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583b1c03380cf86059160cc6c91dcbf56c7b5f141bf3a4f06bc79762d775fac4" +dependencies = [ + "bls12_381", + "lazy_static", + "pairing", + "sha2 0.9.9", +] + +[[package]] +name = "ic0" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a54b5297861c651551676e8c43df805dad175cc33bc97dbd992edbbb85dcbcdf" + +[[package]] +name = "ic_principal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1762deb6f7c8d8c2bdee4b6c5a47b60195b74e9b5280faa5ba29692f8e17429c" +dependencies = [ + "arbitrary", + "crc32fast", + "data-encoding", + "serde", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "icondata" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e75326892b8f4aa213dae3fef9214b9bb5a865a5267ac2143393b72cd78ffa" +dependencies = [ + "icondata_ai", + "icondata_bi", + "icondata_bs", + "icondata_cg", + "icondata_ch", + "icondata_core", + "icondata_fa", + "icondata_fi", + "icondata_hi", "icondata_im", "icondata_io", "icondata_lu", @@ -3221,6 +3703,54 @@ dependencies = [ "icondata_core", ] +[[package]] +name = "icp-ledger" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "comparable", + "crc32fast", + "dfn_candid", + "dfn_core", + "dfn_protobuf", + "hex", + "ic-base-types", + "ic-cdk", + "ic-crypto-sha2", + "ic-ledger-canister-core", + "ic-ledger-core", + "ic-ledger-hash-of", + "icrc-ledger-types", + "lazy_static", + "num-traits", + "on_wire", + "prost 0.12.6", + "prost-derive 0.12.6", + "serde", + "serde_bytes", + "serde_cbor", + "strum", + "strum_macros", +] + +[[package]] +name = "icrc-ledger-types" +version = "0.1.5" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "base32", + "candid", + "crc32fast", + "hex", + "itertools 0.12.1", + "num-bigint", + "num-traits", + "serde", + "serde_bytes", + "sha2 0.10.8", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -3311,7 +3841,7 @@ dependencies = [ "socket2", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] @@ -3347,6 +3877,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -3362,6 +3901,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem 3.0.4", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.3" @@ -3550,7 +4104,7 @@ dependencies = [ "quote", "rstml", "serde", - "syn 2.0.74", + "syn 2.0.76", "walkdir", ] @@ -3591,7 +4145,7 @@ checksum = "90eaea005cabb879c091c84cfec604687ececfd540469e5a30a60c93489a2f23" dependencies = [ "attribute-derive", "cfg-if", - "convert_case", + "convert_case 0.6.0", "html-escape", "itertools 0.12.1", "leptos_hot_reload", @@ -3601,7 +4155,7 @@ dependencies = [ "quote", "rstml", "server_fn_macro", - "syn 2.0.74", + "syn 2.0.76", "tracing", "uuid", ] @@ -3633,7 +4187,7 @@ dependencies = [ "js-sys", "oco_ref", "paste", - "pin-project", + "pin-project 1.1.5", "rustc-hash 1.1.0", "self_cell", "serde", @@ -3698,9 +4252,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.156" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libm" @@ -3777,7 +4331,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -3816,7 +4370,7 @@ dependencies = [ "manyhow-macros", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -3825,11 +4379,17 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64621e2c08f2576e4194ea8be11daf24ac01249a4f53cd8befcbb7077120ead" dependencies = [ - "proc-macro-utils", + "proc-macro-utils 0.8.0", "proc-macro2", "quote", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "match_cfg" version = "0.1.0" @@ -3888,6 +4448,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.2" @@ -4068,6 +4637,11 @@ dependencies = [ "thiserror", ] +[[package]] +name = "on_wire" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" + [[package]] name = "once_cell" version = "1.19.0" @@ -4135,7 +4709,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -4212,9 +4786,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -4261,7 +4835,7 @@ dependencies = [ "regex", "regex-syntax 0.8.4", "structmeta", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -4286,6 +4860,16 @@ dependencies = [ "serde", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4311,6 +4895,16 @@ dependencies = [ "indexmap 2.4.0", ] +[[package]] +name = "phantom_newtype" +version = "0.9.0" +source = "git+https://github.com/dfinity/ic?rev=tags/release-2024-05-29_23-02-base#b9a0f18dd5d6019e3241f205de797bca0d9cc3f8" +dependencies = [ + "candid", + "serde", + "slog", +] + [[package]] name = "phf_shared" version = "0.10.0" @@ -4326,13 +4920,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" +[[package]] +name = "pin-project" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" +dependencies = [ + "pin-project-internal 0.4.30", +] + [[package]] name = "pin-project" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ - "pin-project-internal", + "pin-project-internal 1.1.5", +] + +[[package]] +name = "pin-project-internal" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -4343,7 +4957,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -4435,19 +5049,29 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b55c4d17d994b637e2f4daf6e5dc5d660d209d5642377d675d7a1c3ab69fa579" dependencies = [ - "arrayvec", + "arrayvec 0.5.2", "typed-arena", "unicode-width", ] +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi 0.5.1", +] + [[package]] name = "prettyplease" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -4459,6 +5083,17 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "priority-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560bcab673ff7f6ca9e270c17bf3affd8a05e3bd9207f123b0d45076fd8197e8" +dependencies = [ + "autocfg", + "equivalent", + "indexmap 2.4.0", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -4469,6 +5104,15 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.20", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4485,18 +5129,29 @@ dependencies = [ name = "proc-macro-error-attr" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" dependencies = [ "proc-macro2", "quote", - "version_check", + "smallvec", ] [[package]] name = "proc-macro-utils" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f59e109e2f795a5070e69578c4dc101068139f74616778025ae1011d4cd41a8" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" dependencies = [ "proc-macro2", "quote", @@ -4520,9 +5175,9 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", "version_check", - "yansi", + "yansi 1.0.1", ] [[package]] @@ -4531,6 +5186,16 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + [[package]] name = "prost" version = "0.13.1" @@ -4538,7 +5203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.13.1", ] [[package]] @@ -4549,19 +5214,32 @@ checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" dependencies = [ "bytes", "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.13.0", "log", "multimap", "once_cell", "petgraph", "prettyplease", - "prost", + "prost 0.13.1", "prost-types", "regex", - "syn 2.0.74", + "syn 2.0.76", "tempfile", ] +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.76", +] + [[package]] name = "prost-derive" version = "0.13.1" @@ -4569,10 +5247,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -4581,7 +5259,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" dependencies = [ - "prost", + "prost 0.13.1", ] [[package]] @@ -4593,6 +5271,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -4649,18 +5347,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "quote-use" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e96ac59974192a2fa6ee55a41211cf1385c5b2a8636a4c3068b3b3dd599ece" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" dependencies = [ "quote", "quote-use-macros", @@ -4668,17 +5366,22 @@ dependencies = [ [[package]] name = "quote-use-macros" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c57308e9dde4d7be9af804f6deeaa9951e1de1d5ffce6142eb964750109f7e" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" dependencies = [ - "derive-where", - "proc-macro-utils", + "proc-macro-utils 0.10.0", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -4717,9 +5420,9 @@ checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" [[package]] name = "redb" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6dd20d3cdeb9c7d2366a0b16b93b35b75aec15309fbeb7ce477138c9f68c8c0" +checksum = "58323dc32ea52a8ae105ff94bc0460c5d906307533ba3401aa63db3cbe491fe5" dependencies = [ "libc", ] @@ -4730,7 +5433,7 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" dependencies = [ - "ahash", + "ahash 0.8.11", "arc-swap", "async-trait", "bytes", @@ -4764,9 +5467,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", @@ -4808,6 +5511,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -4846,22 +5558,23 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots 0.25.4", - "winreg 0.50.0", + "winreg", ] [[package]] name = "reqwest" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "hickory-resolver", "http 1.1.0", "http-body 1.0.1", @@ -4899,7 +5612,7 @@ dependencies = [ "wasm-streams", "web-sys", "webpki-roots 0.26.3", - "winreg 0.52.0", + "windows-registry", ] [[package]] @@ -4937,6 +5650,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.6" @@ -4957,6 +5699,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rsb_derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c53e42fccdc5f1172e099785fe78f89bc0c1e657d0c2ef591efbfac427e9a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rstml" version = "0.11.2" @@ -4966,11 +5719,27 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.74", + "syn 2.0.76", "syn_derive", "thiserror", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -5054,9 +5823,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +checksum = "04182dffc9091a404e0fc069ea5cd60e5b866c3adf881eff99a32d048242dffa" dependencies = [ "openssl-probe", "rustls-pemfile 2.1.3", @@ -5117,6 +5886,26 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[package]] +name = "rvs_derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1fa12378eb54f3d4f2db8dcdbe33af610b7e7d001961c1055858282ecef2a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rvstruct" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5107860ec34506b64cf3680458074eac5c2c564f7ccc140918bbcd1714fd8d5d" +dependencies = [ + "rvs_derive", +] + [[package]] name = "ryu" version = "1.0.18" @@ -5141,6 +5930,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.2.0" @@ -5157,6 +5952,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -5172,6 +5973,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secret-vault-value" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc32a777b53b3433b974c9c26b6d502a50037f8da94e46cb8ce2ced2cfdfaea0" +dependencies = [ + "prost 0.13.1", + "prost-types", + "serde", + "serde_json", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -5218,9 +6032,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -5267,20 +6081,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", @@ -5337,7 +6151,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5358,6 +6172,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_tokenstream" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "797ba1d80299b264f3aac68ab5d12e5825a561749db4df7cd7c8083900c5d4e9" +dependencies = [ + "proc-macro2", + "serde", + "syn 1.0.109", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5397,7 +6222,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5435,7 +6260,7 @@ dependencies = [ "serde_qs 0.12.0", "server_fn_macro_default", "thiserror", - "tower", + "tower 0.4.13", "tower-layer", "url", "wasm-bindgen", @@ -5452,10 +6277,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cf0e6f71fc924df36e87f27dfbd447f0bedd092d365db3a5396878256d9f00c" dependencies = [ "const_format", - "convert_case", + "convert_case 0.6.0", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", "xxhash-rust", ] @@ -5466,7 +6291,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e4fd51eb9ee3e7d9fb0febec6cef486dcbc8f7f427591dfcfebee1abe1ad4" dependencies = [ "server_fn_macro", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5524,6 +6349,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + [[package]] name = "simple_asn1" version = "0.6.2" @@ -5563,6 +6394,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" +dependencies = [ + "erased-serde", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -5579,6 +6419,18 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "sns-validation" +version = "0.1.0" +dependencies = [ + "candid", + "humantime", + "regex", + "serde", + "serde_bytes", + "web-time", +] + [[package]] name = "socket2" version = "0.5.7" @@ -5589,6 +6441,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "speedate" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a20480dbd4c693f0b0f3210f2cee5bfa21a176c1fa4df0e65cc0474e7fa557" +dependencies = [ + "strum", + "strum_macros", +] + [[package]] name = "spin" version = "0.9.8" @@ -5607,15 +6469,15 @@ dependencies = [ [[package]] name = "stacker" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -5643,6 +6505,15 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "struct-path" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899edf28cf7320503eda593b4bbce1bc5e9533501a11d45537e2c5be90128fc7" +dependencies = [ + "convert_case 0.6.0", +] + [[package]] name = "structmeta" version = "0.3.0" @@ -5652,7 +6523,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5663,7 +6534,29 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", +] + +[[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 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.76", ] [[package]] @@ -5691,9 +6584,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.74" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -5709,7 +6602,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5723,6 +6616,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "system-configuration" @@ -5745,6 +6641,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.12.0" @@ -5796,7 +6698,7 @@ dependencies = [ "memchr", "parse-display", "pin-project-lite", - "reqwest 0.12.5", + "reqwest 0.12.7", "serde", "serde_json", "serde_with", @@ -5824,7 +6726,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5896,9 +6798,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.39.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" dependencies = [ "backtrace", "bytes", @@ -5920,7 +6822,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -5939,7 +6841,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" dependencies = [ - "pin-project", + "pin-project 1.1.5", "rand", "tokio", ] @@ -6058,7 +6960,7 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "http-body-util", @@ -6066,14 +6968,14 @@ dependencies = [ "hyper-timeout", "hyper-util", "percent-encoding", - "pin-project", - "prost", + "pin-project 1.1.5", + "prost 0.13.1", "rustls-pemfile 2.1.3", "socket2", "tokio", "tokio-rustls 0.26.0", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -6090,7 +6992,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -6108,7 +7010,7 @@ dependencies = [ "http-body-util", "httparse", "js-sys", - "pin-project", + "pin-project 1.1.5", "thiserror", "tonic", "tower-service", @@ -6127,7 +7029,7 @@ dependencies = [ "futures-core", "futures-util", "indexmap 1.9.3", - "pin-project", + "pin-project 1.1.5", "pin-project-lite", "rand", "slab", @@ -6138,6 +7040,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "tower-layer", + "tower-service", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -6175,6 +7087,18 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-util" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1093c19826d33807c72511e68f73b4a0469a3f22c2bd5f7d5212178b4b89674" +dependencies = [ + "futures-core", + "futures-util", + "pin-project 0.4.30", + "tower-service", +] + [[package]] name = "tracing" version = "0.1.40" @@ -6195,7 +7119,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -6236,7 +7160,7 @@ checksum = "1f718dfaf347dcb5b983bfc87608144b0bad87970aebcbea5ce44d2a30c08e63" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -6313,9 +7237,9 @@ checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" [[package]] name = "universal-hash" @@ -6345,6 +7269,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8-width" version = "0.1.7" @@ -6425,7 +7355,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -6459,7 +7389,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6564,6 +7494,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6741,13 +7701,12 @@ dependencies = [ ] [[package]] -name = "winreg" -version = "0.52.0" +name = "wyz" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "tap", ] [[package]] @@ -6756,6 +7715,12 @@ version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yansi" version = "1.0.1" @@ -6780,7 +7745,7 @@ version = "0.1.0" source = "git+https://github.com/go-bazzinga/yral-metadata?rev=c394bf9af3f32d81c1ac50b966c25dafafa2545b#c394bf9af3f32d81c1ac50b966c25dafafa2545b" dependencies = [ "ic-agent", - "reqwest 0.12.5", + "reqwest 0.12.7", "thiserror", "yral-identity", "yral-metadata-types", @@ -6826,7 +7791,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.74", + "syn 2.0.76", ] [[package]] @@ -6834,3 +7799,17 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] diff --git a/Cargo.toml b/Cargo.toml index b746a7dc..62e106f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,18 @@ [workspace] -members = ["ssr"] +members = [ "sns-validation","ssr"] resolver = "2" +[workspace.dependencies] +serde = { version = "1.0", features = ["derive"] } +candid = "0.10.3" +serde_bytes = "0.11.14" +sns-validation.path = "sns-validation" +web-time = "1.0.0" + # Defines a size-optimized profile for the WASM bundle in release mode [profile.wasm-release] inherits = "release" opt-level = 'z' lto = true codegen-units = 1 -panic = "abort" \ No newline at end of file +panic = "abort" diff --git a/fly-prod.toml b/fly-prod.toml index a4589d5d..b9150d1b 100644 --- a/fly-prod.toml +++ b/fly-prod.toml @@ -26,3 +26,5 @@ GOOGLE_REDIRECT_URL = "https://yral.com/auth/google_redirect" GOOGLE_CLIENT_ID = "804814798298-gckvp3hv9sskee5c646b7794k8qolsd7.apps.googleusercontent.com" HOTORNOT_GOOGLE_REDIRECT_URL = "https://hotornot.wtf/auth/google_redirect" HOTORNOT_GOOGLE_CLIENT_ID = "804814798298-bgth3st30cbcgh5qren3i577rgse1va5.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_CLIENT_ID= "804814798298-158b70qepftmlj83aad55thihuq62m1q.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_REDIRECT_URL= "https://icpump.fun/auth/google_redirect" diff --git a/fly-staging.toml b/fly-staging.toml index f81ed1cb..d6403ab6 100644 --- a/fly-staging.toml +++ b/fly-staging.toml @@ -26,3 +26,5 @@ GOOGLE_REDIRECT_URL = "https://hot-or-not-web-leptos-ssr-staging.fly.dev/auth/go GOOGLE_CLIENT_ID = "1000386990382-3012bbnodvsl8jblr0h8b52d9213c7cn.apps.googleusercontent.com" HOTORNOT_GOOGLE_REDIRECT_URL = "https://hotornot.wtf/auth/google_redirect" HOTORNOT_GOOGLE_CLIENT_ID = "804814798298-bgth3st30cbcgh5qren3i577rgse1va5.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_CLIENT_ID= "804814798298-158b70qepftmlj83aad55thihuq62m1q.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_REDIRECT_URL= "https://icpump.fun/auth/google_redirect" diff --git a/fly.toml b/fly.toml index bd9fd593..f720f59b 100644 --- a/fly.toml +++ b/fly.toml @@ -25,3 +25,5 @@ GOOGLE_REDIRECT_URL = "https://hot-or-not-web-leptos-ssr-staging.fly.dev/auth/go GOOGLE_CLIENT_ID = "1000386990382-3012bbnodvsl8jblr0h8b52d9213c7cn.apps.googleusercontent.com" HOTORNOT_GOOGLE_REDIRECT_URL = "https://hotornot.wtf/auth/google_redirect" HOTORNOT_GOOGLE_CLIENT_ID = "804814798298-bgth3st30cbcgh5qren3i577rgse1va5.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_CLIENT_ID= "804814798298-158b70qepftmlj83aad55thihuq62m1q.apps.googleusercontent.com" +ICPUMPFUN_GOOGLE_REDIRECT_URL= "https://icpump.fun/auth/google_redirect" diff --git a/sns-validation/Cargo.toml b/sns-validation/Cargo.toml new file mode 100644 index 00000000..666c3c21 --- /dev/null +++ b/sns-validation/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "sns-validation" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde.workspace = true +serde_bytes.workspace = true +candid.workspace = true +humantime = "2.1.0" +regex = "1.10.5" +web-time.workspace = true \ No newline at end of file diff --git a/sns-validation/src/config.rs b/sns-validation/src/config.rs new file mode 100644 index 00000000..8371b206 --- /dev/null +++ b/sns-validation/src/config.rs @@ -0,0 +1,773 @@ +use candid::Principal; +use std::{fmt::Debug, str::FromStr}; + +use crate::{ + humanize, + pbs::{ + gov_pb::CreateServiceNervousSystem, + nns_pb::{self, GlobalTimeOfDay, Image}, + sns_pb::SnsInitPayload, + ExecutedCreateServiceNervousSystemProposal, + }, +}; +use web_time::{SystemTime, UNIX_EPOCH}; + +// Alias CreateServiceNervousSystem-related types, but since we have many +// related types in this module, put these aliases in their own module to avoid +// getting mixed up. +mod nns_governance_pb { + pub use crate::pbs::gov_pb::create_sns::{ + governance_parameters::VotingRewardParameters, + initial_token_distribution::{ + developer_distribution::NeuronDistribution, DeveloperDistribution, SwapDistribution, + TreasuryDistribution, + }, + swap_parameters::NeuronBasketConstructionParameters, + GovernanceParameters, InitialTokenDistribution, LedgerParameters, SwapParameters, + }; +} + +// Implements the format used by test_sns_init_v2.yaml in the root of this +// package. Studying that is a much more ergonomic way of becoming familiar with +// the format that we are trying to implement here. +// +// (Thanks to the magic of serde, all the code here is declarative.) +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct SnsConfigurationFile { + pub name: String, + pub description: String, + pub logo_b64: String, + pub url: String, + + #[serde(rename = "Principals", default)] + pub principals: Vec, + + pub fallback_controller_principals: Vec, // Principal (alias) + pub dapp_canisters: Vec, // Principal (alias) + + #[serde(rename = "Token")] + pub token: Token, + + #[serde(rename = "Proposals")] + pub proposals: Proposals, + + #[serde(rename = "Neurons")] + pub neurons: Neurons, + + #[serde(rename = "Voting")] + pub voting: Voting, + + #[serde(rename = "Distribution")] + pub distribution: Distribution, + + #[serde(rename = "Swap")] + pub swap: Swap, + + #[serde(rename = "NnsProposal")] + pub nns_proposal: NnsProposal, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct PrincipalAlias { + id: String, // PrincipalId + name: Option, + email: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Token { + pub name: String, + pub symbol: String, + #[serde(with = "humanize::ser_de::tokens")] + pub transaction_fee: nns_pb::Tokens, + pub logo_b64: String, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Proposals { + #[serde(with = "humanize::ser_de::tokens")] + pub rejection_fee: nns_pb::Tokens, + + #[serde(with = "humanize::ser_de::duration")] + pub initial_voting_period: nns_pb::Duration, + + #[serde(with = "humanize::ser_de::duration")] + pub maximum_wait_for_quiet_deadline_extension: nns_pb::Duration, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Neurons { + #[serde(with = "humanize::ser_de::tokens")] + pub minimum_creation_stake: nns_pb::Tokens, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Voting { + #[serde(with = "humanize::ser_de::duration")] + pub minimum_dissolve_delay: nns_pb::Duration, + + #[serde(rename = "MaximumVotingPowerBonuses")] + pub maximum_voting_power_bonuses: MaximumVotingPowerBonuses, + + #[serde(rename = "RewardRate")] + pub reward_rate: RewardRate, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct MaximumVotingPowerBonuses { + #[serde(rename = "DissolveDelay")] + pub dissolve_delay: Bonus, + + #[serde(rename = "Age")] + pub age: Bonus, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Bonus { + #[serde(with = "humanize::ser_de::duration")] + pub duration: nns_pb::Duration, + + #[serde(with = "humanize::ser_de::percentage")] + pub bonus: nns_pb::Percentage, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct RewardRate { + #[serde(with = "humanize::ser_de::percentage")] + pub initial: nns_pb::Percentage, + + #[serde(with = "humanize::ser_de::percentage")] + pub r#final: nns_pb::Percentage, + + #[serde(with = "humanize::ser_de::duration")] + pub transition_duration: nns_pb::Duration, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Swap { + pub minimum_participants: u64, + + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_tokens")] + pub minimum_icp: Option, + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_tokens")] + pub maximum_icp: Option, + + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_tokens")] + pub minimum_direct_participation_icp: Option, + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_tokens")] + pub maximum_direct_participation_icp: Option, + + #[serde(with = "humanize::ser_de::tokens")] + pub minimum_participant_icp: nns_pb::Tokens, + #[serde(with = "humanize::ser_de::tokens")] + pub maximum_participant_icp: nns_pb::Tokens, + + pub confirmation_text: Option, + pub restricted_countries: Option>, + + #[serde(rename = "VestingSchedule")] + pub vesting_schedule: VestingSchedule, + + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_time_of_day")] + pub start_time: Option, + #[serde(with = "humanize::ser_de::duration")] + pub duration: nns_pb::Duration, + + #[serde(default)] + #[serde(with = "humanize::ser_de::optional_tokens")] + pub neurons_fund_investment_icp: Option, + + #[serde(default)] + pub neurons_fund_participation: Option, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct VestingSchedule { + pub events: u64, + + #[serde(with = "humanize::ser_de::duration")] + pub interval: nns_pb::Duration, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Distribution { + #[serde(rename = "Neurons")] + pub neurons: Vec, + + #[serde(rename = "InitialBalances")] + pub initial_balances: InitialBalances, + + #[serde(with = "humanize::ser_de::tokens")] + pub total: nns_pb::Tokens, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Neuron { + pub principal: String, // Principal (alias) + + #[serde(with = "humanize::ser_de::tokens")] + pub stake: nns_pb::Tokens, + + #[serde(default)] + pub memo: u64, + + #[serde(with = "humanize::ser_de::duration")] + pub dissolve_delay: nns_pb::Duration, + + #[serde(with = "humanize::ser_de::duration")] + pub vesting_period: nns_pb::Duration, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +pub struct InitialBalances { + #[serde(with = "humanize::ser_de::tokens")] + pub governance: nns_pb::Tokens, + + #[serde(with = "humanize::ser_de::tokens")] + pub swap: nns_pb::Tokens, +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, PartialEq, Eq, Clone)] +pub struct NnsProposal { + pub title: String, + pub summary: String, + pub url: Option, +} + +struct AliasToPrincipalId<'a> { + #[allow(unused)] + source: &'a Vec, + /* TODO + #[derive(Debug, PartialEq, Eq, Hash)] + enum Key { // TODO: This name is just a placeholder. + Name(String), + Email(String), + } + + alias_to_principal_id: HashMap, + */ +} + +impl<'a> AliasToPrincipalId<'a> { + fn new(source: &'a Vec) -> Self { + Self { source } + } + + /// TODO: Currently, this just does PrincipalId::from_str, but real alias + /// substitution is planned for a future MR. + fn unalias( + &self, + field_name: &str, + principals: &[String], + ) -> Result, Vec> { + let mut defects = vec![]; + + let result = principals + .iter() + .map(|string| { + Principal::from_str(string) + .map_err(|err| { + defects.push(format!( + "Unable to parse PrincipalId ({:?}) in {}. Reason: {}", + string, field_name, err, + )) + }) + .unwrap_or(Principal::anonymous()) + }) + .collect(); + + if !defects.is_empty() { + return Err(defects); + } + + Ok(result) + } +} + +impl SnsConfigurationFile { + pub fn try_convert_to_create_service_nervous_system( + &self, + ) -> Result { + // Step 1: Unpack. + let SnsConfigurationFile { + name, + description, + logo_b64, + url, + principals, + fallback_controller_principals, + dapp_canisters, + token, + proposals, + neurons, + voting, + distribution, + swap, + nns_proposal: _, // We ignore the NNS Proposal fields + } = self; + + // Step 2: Convert components. + // + // (This is the main section, where the "real" work takes place.) + let alias_to_principal_id = AliasToPrincipalId::new(principals); + let mut defects = vec![]; + + // 2.1: Convert "primitive" typed fields. + + let name = Some(name.clone()); + let description = Some(description.clone()); + let url = Some(url.clone()); + + // 2.2: Convert Vec fields. + + let fallback_controller_principal_ids = alias_to_principal_id + .unalias( + "fallback_controller_principals", + fallback_controller_principals, + ) + .map_err(|inner_defects| defects.extend(inner_defects)) + .unwrap_or_default(); + + let dapp_canisters = alias_to_principal_id + .unalias("dapp_canisters", dapp_canisters) + .map_err(|inner_defects| defects.extend(inner_defects)) + .unwrap_or_default(); + + // Wrap in Canister. + let dapp_canisters = dapp_canisters + .into_iter() + .map(|principal_id| { + let id = Some(principal_id); + nns_pb::Canister { id } + }) + .collect(); + + // 2.3: Convert composite fields. + let initial_token_distribution = Some( + distribution + .try_convert_to_initial_token_distribution() + .map_err(|inner_defects| defects.extend(inner_defects)) + .unwrap_or_default(), + ); + let swap_parameters = Some(swap.convert_to_swap_parameters()); + let ledger_parameters = Some(token.convert_to_ledger_parameters()); + let governance_parameters = + Some(convert_to_governance_parameters(proposals, neurons, voting)); + + // Step 3: Repackage. + let result = CreateServiceNervousSystem { + name, + description, + url, + logo: Some(Image { + base64_encoding: Some(logo_b64.clone()), + }), + + fallback_controller_principal_ids, + dapp_canisters, + + initial_token_distribution, + swap_parameters, + ledger_parameters, + governance_parameters, + }; + + // Step 4: Validate. + if !defects.is_empty() { + return Err(format!( + "Unable to convert configuration file to proposal for the following \ + reason(s):\n -{}", + defects.join("\n -"), + )); + } + if let Err(err) = SnsInitPayload::try_from(result.clone()) { + return Err(format!( + "Unable to convert configuration file to proposal: {}", + err, + )); + } + + // Step 5: Ship it! + Ok(result) + } + + pub fn try_convert_to_executed_sns_init(&self) -> Result { + let create_sns = self.try_convert_to_create_service_nervous_system()?; + let executed_create_sns = ExecutedCreateServiceNervousSystemProposal { + current_timestamp_seconds: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + create_service_nervous_system: create_sns, + random_swap_start_time: GlobalTimeOfDay { + seconds_after_utc_midnight: Some(0), + }, + neurons_fund_participation_constraints: None, + // `proposal_id` only exists to be exposed to the user for audit purposes, which don't apply here. + // But it's required, so we can just use any arbitrary value. + proposal_id: 10, + }; + + SnsInitPayload::try_from(executed_create_sns) + } +} + +impl Distribution { + fn try_convert_to_initial_token_distribution( + &self, + ) -> Result> { + let Distribution { + neurons, + initial_balances, + total, + } = self; + + let mut defects = vec![]; + // IDEALLY: Make Tokens support operators like +, -, and *. Ditto for + // Duration, Percentage. + let mut observed_total_e8s = 0; + + let developer_distribution = + try_convert_from_neuron_vec_to_developer_distribution_and_total_stake(neurons) + .map_err(|inner_defects| defects.extend(inner_defects)) + .unwrap_or_default(); + observed_total_e8s += developer_distribution + .developer_neurons + .iter() + .map(|developer_neuron| { + developer_neuron + .stake + .unwrap_or_default() + .e8s + .unwrap_or_default() + }) + .sum::(); + let developer_distribution = Some(developer_distribution); + + let (treasury_distribution, swap_distribution) = { + let InitialBalances { governance, swap } = initial_balances; + + observed_total_e8s += governance.e8s.unwrap_or_default(); + observed_total_e8s += swap.e8s.unwrap_or_default(); + + ( + Some(nns_governance_pb::TreasuryDistribution { + total: Some(*governance), + }), + Some(nns_governance_pb::SwapDistribution { total: Some(*swap) }), + ) + }; + + // Validate total SNS tokens. + if observed_total_e8s != total.e8s.unwrap_or_default() { + defects.push(format!( + "The total amount of SNS tokens was expected to be {}, but was instead {}.", + humanize::format_tokens(total), + humanize::format_tokens(&nns_pb::Tokens { + e8s: Some(observed_total_e8s), + }), + )); + } + + if !defects.is_empty() { + return Err(defects); + } + + Ok(nns_governance_pb::InitialTokenDistribution { + developer_distribution, + treasury_distribution, + swap_distribution, + }) + } +} + +fn try_convert_from_neuron_vec_to_developer_distribution_and_total_stake( + original: &[Neuron], +) -> Result> { + let mut defects = vec![]; + + let developer_neurons = original + .iter() + .map(|neuron| { + neuron + .try_convert_to_neuron_distribution() + .map_err(|inner_defects| defects.extend(inner_defects)) + .unwrap_or_default() + }) + .collect(); + + if !defects.is_empty() { + return Err(defects); + } + + Ok(nns_governance_pb::DeveloperDistribution { developer_neurons }) +} + +impl Neuron { + fn try_convert_to_neuron_distribution( + &self, + ) -> Result> { + let Neuron { + principal, + stake, + memo, + dissolve_delay, + vesting_period, + } = self; + + let mut defects = vec![]; + + let controller = Principal::from_str(principal) + .map_err(|err| { + defects.push(format!( + "Unable to parse PrincipalId in distribution.neurons ({:?}). \ + err: {:#?}", + principal, err, + )) + }) + .unwrap_or(Principal::anonymous()); + let controller = Some(controller); + + let dissolve_delay = Some(*dissolve_delay); + let memo = Some(*memo); + let stake = Some(*stake); + + let vesting_period = Some(*vesting_period); + + if !defects.is_empty() { + return Err(defects); + } + + Ok(nns_governance_pb::NeuronDistribution { + controller, + dissolve_delay, + memo, + stake, + vesting_period, + }) + } +} + +impl Token { + fn convert_to_ledger_parameters(&self) -> nns_governance_pb::LedgerParameters { + let Token { + name, + symbol, + transaction_fee, + logo_b64, + } = self; + + let token_name = Some(name.clone()); + let token_symbol = Some(symbol.clone()); + let transaction_fee = Some(*transaction_fee); + + let token_logo = logo_b64.clone(); + + nns_governance_pb::LedgerParameters { + token_name, + token_symbol, + transaction_fee, + token_logo: Some(Image { + base64_encoding: Some(token_logo), + }), + } + } +} + +fn convert_to_governance_parameters( + proposals: &Proposals, + neurons: &Neurons, + voting: &Voting, +) -> nns_governance_pb::GovernanceParameters { + let Proposals { + rejection_fee, + initial_voting_period, + maximum_wait_for_quiet_deadline_extension, + } = proposals; + let Neurons { + minimum_creation_stake, + } = neurons; + let Voting { + minimum_dissolve_delay, + maximum_voting_power_bonuses, + reward_rate, + } = voting; + let MaximumVotingPowerBonuses { + dissolve_delay, + age, + } = maximum_voting_power_bonuses; + + let proposal_rejection_fee = Some(*rejection_fee); + let proposal_initial_voting_period = Some(*initial_voting_period); + let proposal_wait_for_quiet_deadline_increase = + Some(*maximum_wait_for_quiet_deadline_extension); + + let neuron_minimum_stake = Some(*minimum_creation_stake); + let neuron_minimum_dissolve_delay_to_vote = Some(*minimum_dissolve_delay); + + let (neuron_maximum_dissolve_delay, neuron_maximum_dissolve_delay_bonus) = { + let Bonus { duration, bonus } = dissolve_delay; + + (Some(*duration), Some(*bonus)) + }; + + let (neuron_maximum_age_for_age_bonus, neuron_maximum_age_bonus) = { + let Bonus { duration, bonus } = age; + + (Some(*duration), Some(*bonus)) + }; + + let voting_reward_parameters = Some(reward_rate.convert_to_voting_reward_parameters()); + + nns_governance_pb::GovernanceParameters { + proposal_rejection_fee, + proposal_initial_voting_period, + proposal_wait_for_quiet_deadline_increase, + + neuron_minimum_stake, + + neuron_minimum_dissolve_delay_to_vote, + neuron_maximum_dissolve_delay, + neuron_maximum_dissolve_delay_bonus, + + neuron_maximum_age_for_age_bonus, + neuron_maximum_age_bonus, + + voting_reward_parameters, + } +} + +impl RewardRate { + fn convert_to_voting_reward_parameters(&self) -> nns_governance_pb::VotingRewardParameters { + let RewardRate { + initial, + r#final, + transition_duration, + } = self; + + let initial_reward_rate = Some(*initial); + let final_reward_rate = Some(*r#final); + let reward_rate_transition_duration = Some(*transition_duration); + + nns_governance_pb::VotingRewardParameters { + initial_reward_rate, + final_reward_rate, + reward_rate_transition_duration, + } + } +} + +impl Swap { + fn convert_to_swap_parameters(&self) -> nns_governance_pb::SwapParameters { + let Swap { + minimum_participants, + + minimum_icp, + maximum_icp, + + minimum_direct_participation_icp, + maximum_direct_participation_icp, + + maximum_participant_icp, + minimum_participant_icp, + + confirmation_text, + restricted_countries, + + vesting_schedule, + + start_time, + duration, + neurons_fund_investment_icp, + neurons_fund_participation, + } = self; + + let minimum_participants = Some(*minimum_participants); + + let minimum_icp = *minimum_icp; + let maximum_icp = *maximum_icp; + + let minimum_direct_participation_icp = minimum_direct_participation_icp + .or_else(|| minimum_icp?.checked_sub(&neurons_fund_investment_icp.unwrap_or_default())); + let maximum_direct_participation_icp = maximum_direct_participation_icp + .or_else(|| maximum_icp?.checked_sub(&neurons_fund_investment_icp.unwrap_or_default())); + + let maximum_participant_icp = Some(*maximum_participant_icp); + let minimum_participant_icp = Some(*minimum_participant_icp); + + let confirmation_text = confirmation_text.clone(); + let restricted_countries = + restricted_countries + .as_ref() + .map(|restricted_countries| nns_pb::Countries { + iso_codes: restricted_countries.clone(), + }); + + let neuron_basket_construction_parameters = + Some(vesting_schedule.convert_to_neuron_basket_construction_parameters()); + + let start_time = *start_time; + let duration = Some(*duration); + + let neurons_fund_participation = *neurons_fund_participation; + + nns_governance_pb::SwapParameters { + minimum_participants, + + minimum_icp, + maximum_icp, + + minimum_direct_participation_icp, + maximum_direct_participation_icp, + + maximum_participant_icp, + minimum_participant_icp, + + neuron_basket_construction_parameters, + + confirmation_text, + restricted_countries, + + start_time, + duration, + + neurons_fund_investment_icp: *neurons_fund_investment_icp, + neurons_fund_participation, + } + } +} + +impl VestingSchedule { + fn convert_to_neuron_basket_construction_parameters( + &self, + ) -> nns_governance_pb::NeuronBasketConstructionParameters { + let VestingSchedule { events, interval } = self; + + let count = Some(*events); + let dissolve_delay_interval = Some(*interval); + + nns_governance_pb::NeuronBasketConstructionParameters { + count, + dissolve_delay_interval, + } + } +} diff --git a/sns-validation/src/consts.rs b/sns-validation/src/consts.rs new file mode 100644 index 00000000..066125d9 --- /dev/null +++ b/sns-validation/src/consts.rs @@ -0,0 +1,4 @@ +pub const E8S_PER_TOKEN: u64 = 100_000_000; +pub const ONE_DAY_SECONDS: u64 = 24 * 60 * 60; +pub const ONE_YEAR_SECONDS: u64 = (4 * 365 + 1) * ONE_DAY_SECONDS / 4; +pub const ONE_MONTH_SECONDS: u64 = ONE_YEAR_SECONDS / 12; diff --git a/sns-validation/src/humanize/mod.rs b/sns-validation/src/humanize/mod.rs new file mode 100644 index 00000000..7af7a153 --- /dev/null +++ b/sns-validation/src/humanize/mod.rs @@ -0,0 +1,223 @@ +pub mod ser_de; + +use std::{collections::VecDeque, fmt::Display, str::FromStr, sync::LazyLock}; + +use regex::Regex; + +use crate::pbs::nns_pb; + +// Normally, we would import this from ic_nervous_system_common, but we'd be +// dragging in lots of stuff along with it. The main problem with that is that +// any random change that requires ic_nervous_system_common to be rebuilt will +// also trigger a rebuild here. This gives us a "fire wall" to prevent fires +// from spreading. +// +// TODO(NNS1-2284): Move E8, and other such things to their own tiny library to +// avoid triggering mass rebuilds. +pub(crate) const E8: u64 = 100_000_000; + +pub fn parse_tokens(s: &str) -> Result { + let e8s = if let Some(s) = s.strip_suffix("tokens").map(|s| s.trim()) { + parse_fixed_point_decimal(s, /* decimal_places = */ 8)? + } else if let Some(s) = s.strip_suffix("token").map(|s| s.trim()) { + parse_fixed_point_decimal(s, /* decimal_places = */ 8)? + } else if let Some(s) = s.strip_suffix("e8s").map(|s| s.trim()) { + u64::from_str(&s.replace('_', "")).map_err(|err| err.to_string())? + } else { + return Err(format!("Invalid tokens input string: {}", s)); + }; + let e8s = Some(e8s); + + Ok(nns_pb::Tokens { e8s }) +} + +pub fn parse_duration(s: &str) -> Result { + humantime::parse_duration(s) + .map(|d| nns_pb::Duration { + seconds: Some(d.as_secs()), + }) + .map_err(|err| err.to_string()) +} + +pub fn parse_percentage(s: &str) -> Result { + let number = s + .strip_suffix('%') + .ok_or_else(|| format!("Input string must end with a percent sign: {}", s))?; + + let basis_points = Some(parse_fixed_point_decimal( + number, /* decimal_places = */ 2, + )?); + Ok(nns_pb::Percentage { basis_points }) +} + +pub fn parse_time_of_day(s: &str) -> Result { + const FORMAT: &str = "hh:mm UTC"; + let error = format!("Unable to parse time of day \"{s}\". Format should be \"{FORMAT}\"",); + + // decompose "hh:mm UTC" into ["hh:mm", "UTC"] + let parts = s.split_whitespace().collect::>(); + let [hh_mm, "UTC"] = &parts[..] else { + return Err(error); + }; + + // decompose "hh:mm" into ["hh", "mm"] + let parts = hh_mm.split(':').collect::>(); + let [hh, mm] = &parts[..] else { + return Err(error); + }; + if hh.len() != 2 || mm.len() != 2 { + return Err(error); + } + + // convert ["hh", "mm"] into hh, mm + let Ok(hh) = u64::from_str(hh) else { + return Err(error); + }; + let Ok(mm) = u64::from_str(mm) else { + return Err(error); + }; + + nns_pb::GlobalTimeOfDay::from_hh_mm(hh, mm) +} + +static FP_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?x) # Verbose (ignore white space, and comments, like this). + ^ # begin + (?P[\d_]+) # Digit or underscores (for grouping digits). + ( # The dot + fractional part... + [.] # dot + (?P[\d_]+) + )? # ... is optional. + $ # end +", + ) + .unwrap() +}); + +fn parse_fixed_point_decimal(s: &str, decimal_places: usize) -> Result { + let found = FP_REGEX + .captures(s) + .ok_or_else(|| format!("Not a number: {}", s))?; + + let whole = u64::from_str( + &found + .name("whole") + .expect("Missing capture group?!") + .as_str() + .replace('_', ""), + ) + .map_err(|err| err.to_string())?; + + let fractional = format!( + // Pad so that fractional ends up being of length (at least) decimal_places. + "{:0 decimal_places { + return Err(format!("Too many digits after the decimal place: {}", s)); + } + let fractional = u64::from_str(&fractional).map_err(|err| err.to_string())?; + + Ok(shift_decimal_right(whole, decimal_places)? + fractional) +} + +fn shift_decimal_right(n: u64, count: I) -> Result +where + u32: TryFrom, + >::Error: Display, + I: Display + Copy, +{ + let count = u32::try_from(count) + .map_err(|err| format!("Unable to convert {} to u32. Reason: {}", count, err))?; + + let boost = 10_u64 + .checked_pow(count) + .ok_or_else(|| format!("Too large of an exponent: {}", count))?; + + n.checked_mul(boost) + .ok_or_else(|| format!("Too large of a decimal shift: {} >> {}", n, count)) +} + +pub fn format_tokens(tokens: &nns_pb::Tokens) -> String { + let nns_pb::Tokens { e8s } = tokens; + let e8s = e8s.unwrap_or(0); + + if 0 < e8s && e8s < 1_000_000 { + return format!("{} e8s", group_digits(e8s)); + } + + // TODO: format_fixed_point_decimal. parse_fixed_point_decimal seems + // lonesome. But seriously, it can also be used in format_percentage. + + let whole = e8s / E8; + let fractional = e8s % E8; + + let fractional = if fractional == 0 { + "".to_string() + } else { + // TODO: Group. + format!(".{:08}", fractional).trim_matches('0').to_string() + }; + + let units = if e8s == E8 { "token" } else { "tokens" }; + + format!("{}{} {}", group_digits(whole), fractional, units) +} + +pub fn format_duration(duration: &nns_pb::Duration) -> String { + let nns_pb::Duration { seconds } = duration; + let seconds = seconds.unwrap_or(0); + + humantime::format_duration(std::time::Duration::from_secs(seconds)).to_string() +} + +pub fn format_percentage(percentage: &nns_pb::Percentage) -> String { + let nns_pb::Percentage { basis_points } = percentage; + let basis_points = basis_points.unwrap_or(0); + + let whole = basis_points / 100; + let fractional = basis_points % 100; + + let fractional = if fractional == 0 { + "".to_string() + } else { + format!(".{:02}", fractional).trim_matches('0').to_string() + }; + + format!("{}{}%", group_digits(whole), fractional) +} + +pub fn format_time_of_day(time_of_day: &nns_pb::GlobalTimeOfDay) -> String { + let (hours, minutes) = time_of_day.as_hh_mm().unwrap_or((0, 0)); + + format!("{hours:02}:{minutes:02} UTC") +} + +pub(crate) fn group_digits(n: u64) -> String { + let mut left_todo = n; + let mut groups = VecDeque::new(); + + while left_todo > 0 { + let group = left_todo % 1000; + left_todo /= 1000; + + let group = if left_todo == 0 { + format!("{}", group) + } else { + format!("{:03}", group) + }; + + groups.push_front(group); + } + + if groups.is_empty() { + return "0".to_string(); + } + + Vec::from(groups).join("_") +} diff --git a/sns-validation/src/humanize/ser_de.rs b/sns-validation/src/humanize/ser_de.rs new file mode 100644 index 00000000..2104a9db --- /dev/null +++ b/sns-validation/src/humanize/ser_de.rs @@ -0,0 +1,144 @@ +use super::*; +use crate::pbs::nns_pb; +use serde::{ser::Error, Deserialize, Deserializer, Serializer}; + +pub mod tokens { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + parse_tokens(&string).map_err(serde::de::Error::custom) + } + + pub fn serialize(tokens: &nns_pb::Tokens, serializer: S) -> Result + where + S: Serializer, + { + if tokens.e8s.is_none() { + return Err(S::Error::custom( + "Unable to format Tokens, because e8s is blank.", + )); + } + serializer.serialize_str(&format_tokens(tokens)) + } +} + +pub mod duration { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + parse_duration(&string).map_err(serde::de::Error::custom) + } + + pub fn serialize(duration: &nns_pb::Duration, serializer: S) -> Result + where + S: Serializer, + { + if duration.seconds.is_none() { + return Err(S::Error::custom( + "Unable to format Duration, because seconds is blank.", + )); + } + serializer.serialize_str(&format_duration(duration)) + } +} + +pub mod percentage { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string: String = Deserialize::deserialize(deserializer)?; + parse_percentage(&string).map_err(serde::de::Error::custom) + } + + pub fn serialize(percentage: &nns_pb::Percentage, serializer: S) -> Result + where + S: Serializer, + { + if percentage.basis_points.is_none() { + return Err(S::Error::custom( + "Unable to format Percentage, because basis_points is blank.", + )); + } + serializer.serialize_str(&format_percentage(percentage)) + } +} + +pub mod optional_tokens { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let string: Option = Deserialize::deserialize(deserializer)?; + string + .map(|string| parse_tokens(&string).map_err(serde::de::Error::custom)) + .transpose() + } + + pub fn serialize(tokens: &Option, serializer: S) -> Result + where + S: Serializer, + { + match tokens { + None => serializer.serialize_none(), + Some(tokens) => tokens::serialize(tokens, serializer), + } + } +} + +pub mod optional_time_of_day { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let string: Option = Deserialize::deserialize(deserializer)?; + + let string = match string { + None => return Ok(None), + Some(string) => string, + }; + + let global_time_of_day = parse_time_of_day(&string).map_err(serde::de::Error::custom)?; + Ok(Some(global_time_of_day)) + } + + pub fn serialize( + time_of_day: &Option, + serializer: S, + ) -> Result + where + S: Serializer, + { + let time_of_day = match time_of_day.as_ref() { + None => return serializer.serialize_none(), + Some(time_of_day) => time_of_day, + }; + + // Input was Some -> format it (i.e. convert to String). + if time_of_day.seconds_after_utc_midnight.is_none() { + return Err(S::Error::custom( + "Unable to format TimeOfDay, because seconds_after_utc_midnight is blank.", + )); + } + let string = format_time_of_day(time_of_day); + + // The string needs to be wrapped in Some. Otherwise, the round trip is + // going to be missing an Option layer: look at the first line of + // deserialize: we try to get an Option from deserializer. + serializer.serialize_some(&Some(string)) + } +} diff --git a/sns-validation/src/lib.rs b/sns-validation/src/lib.rs new file mode 100644 index 00000000..9a7913c3 --- /dev/null +++ b/sns-validation/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config; +mod consts; +pub mod humanize; +pub mod pbs; +mod validation; diff --git a/sns-validation/src/pbs/gov_pb.rs b/sns-validation/src/pbs/gov_pb.rs new file mode 100644 index 00000000..d6581f65 --- /dev/null +++ b/sns-validation/src/pbs/gov_pb.rs @@ -0,0 +1,236 @@ +use std::collections::BTreeMap; + +#[derive(candid::CandidType, candid::Deserialize, Eq, std::hash::Hash)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq)] +pub struct NeuronId { + #[serde(with = "serde_bytes")] + pub id: Vec, +} + +#[derive(candid::CandidType, candid::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq)] +pub struct NervousSystemParameters { + pub reject_cost_e8s: ::core::option::Option, + + pub neuron_minimum_stake_e8s: ::core::option::Option, + + pub transaction_fee_e8s: ::core::option::Option, + + pub max_proposals_to_keep_per_action: ::core::option::Option, + + pub initial_voting_period_seconds: ::core::option::Option, + + pub wait_for_quiet_deadline_increase_seconds: ::core::option::Option, + + pub default_followees: ::core::option::Option, + + pub max_number_of_neurons: ::core::option::Option, + + pub neuron_minimum_dissolve_delay_to_vote_seconds: ::core::option::Option, + + pub max_followees_per_function: ::core::option::Option, + + pub max_dissolve_delay_seconds: ::core::option::Option, + + pub max_neuron_age_for_age_bonus: ::core::option::Option, + + pub max_number_of_proposals_with_ballots: ::core::option::Option, + + pub neuron_claimer_permissions: ::core::option::Option, + + pub neuron_grantable_permissions: ::core::option::Option, + + pub max_number_of_principals_per_neuron: ::core::option::Option, + + pub voting_rewards_parameters: ::core::option::Option, + + pub max_dissolve_delay_bonus_percentage: ::core::option::Option, + + pub max_age_bonus_percentage: ::core::option::Option, + + pub maturity_modulation_disabled: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq)] +pub struct VotingRewardsParameters { + pub round_duration_seconds: ::core::option::Option, + + pub reward_rate_transition_duration_seconds: ::core::option::Option, + + pub initial_reward_rate_basis_points: ::core::option::Option, + pub final_reward_rate_basis_points: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Default)] +pub struct DefaultFollowees { + pub followees: BTreeMap, +} + +#[derive(candid::CandidType, candid::Deserialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Default)] +pub struct NeuronPermissionList { + pub permissions: Vec, +} + +pub mod neuron { + + #[derive(candid::CandidType, candid::Deserialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq)] + pub struct Followees { + pub followees: Vec, + } + + #[derive(candid::CandidType, candid::Deserialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq)] + pub enum DissolveState { + WhenDissolvedTimestampSeconds(u64), + + DissolveDelaySeconds(u64), + } +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq)] +pub struct CreateServiceNervousSystem { + pub name: ::core::option::Option, + pub description: ::core::option::Option, + pub url: ::core::option::Option, + pub logo: ::core::option::Option, + pub fallback_controller_principal_ids: Vec, + pub dapp_canisters: Vec, + pub initial_token_distribution: ::core::option::Option, + pub swap_parameters: ::core::option::Option, + pub ledger_parameters: ::core::option::Option, + pub governance_parameters: ::core::option::Option, +} + +pub mod create_sns { + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct InitialTokenDistribution { + pub developer_distribution: + ::core::option::Option, + pub treasury_distribution: + ::core::option::Option, + pub swap_distribution: ::core::option::Option, + } + + pub mod initial_token_distribution { + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct DeveloperDistribution { + pub developer_neurons: Vec, + } + + pub mod developer_distribution { + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct NeuronDistribution { + pub controller: ::core::option::Option, + pub dissolve_delay: ::core::option::Option, + pub memo: ::core::option::Option, + pub stake: ::core::option::Option, + pub vesting_period: ::core::option::Option, + } + } + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct TreasuryDistribution { + pub total: ::core::option::Option, + } + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct SwapDistribution { + pub total: ::core::option::Option, + } + } + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct SwapParameters { + pub minimum_participants: ::core::option::Option, + pub minimum_icp: ::core::option::Option, + pub maximum_icp: ::core::option::Option, + pub minimum_direct_participation_icp: ::core::option::Option, + pub maximum_direct_participation_icp: ::core::option::Option, + pub minimum_participant_icp: ::core::option::Option, + pub maximum_participant_icp: ::core::option::Option, + pub neuron_basket_construction_parameters: + ::core::option::Option, + pub confirmation_text: ::core::option::Option, + pub restricted_countries: ::core::option::Option, + + pub start_time: ::core::option::Option, + pub duration: ::core::option::Option, + + pub neurons_fund_investment_icp: ::core::option::Option, + + pub neurons_fund_participation: ::core::option::Option, + } + + pub mod swap_parameters { + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct NeuronBasketConstructionParameters { + pub count: ::core::option::Option, + pub dissolve_delay_interval: ::core::option::Option, + } + } + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct LedgerParameters { + pub transaction_fee: ::core::option::Option, + pub token_name: ::core::option::Option, + pub token_symbol: ::core::option::Option, + pub token_logo: ::core::option::Option, + } + + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct GovernanceParameters { + pub proposal_rejection_fee: ::core::option::Option, + pub proposal_initial_voting_period: ::core::option::Option, + pub proposal_wait_for_quiet_deadline_increase: + ::core::option::Option, + pub neuron_minimum_stake: ::core::option::Option, + pub neuron_minimum_dissolve_delay_to_vote: + ::core::option::Option, + pub neuron_maximum_dissolve_delay: ::core::option::Option, + pub neuron_maximum_dissolve_delay_bonus: + ::core::option::Option, + pub neuron_maximum_age_for_age_bonus: ::core::option::Option, + pub neuron_maximum_age_bonus: ::core::option::Option, + pub voting_reward_parameters: + ::core::option::Option, + } + + pub mod governance_parameters { + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Default)] + pub struct VotingRewardParameters { + pub initial_reward_rate: ::core::option::Option, + pub final_reward_rate: ::core::option::Option, + pub reward_rate_transition_duration: + ::core::option::Option, + } + } +} diff --git a/sns-validation/src/pbs/mod.rs b/sns-validation/src/pbs/mod.rs new file mode 100644 index 00000000..2a416da6 --- /dev/null +++ b/sns-validation/src/pbs/mod.rs @@ -0,0 +1,597 @@ +use gov_pb::{create_sns, CreateServiceNervousSystem}; +use nns_pb::{Duration, GlobalTimeOfDay}; +use sns_pb::{sns_init_payload, SnsInitPayload}; +use sns_swap_pb::NeuronsFundParticipationConstraints; + +use crate::consts::ONE_DAY_SECONDS; + +pub(crate) mod gov_pb; +pub mod nns_pb; +pub mod sns_pb; +pub(crate) mod sns_swap_pb; + +fn divide_perfectly(field_name: &str, dividend: u64, divisor: u64) -> Result { + match dividend.checked_rem(divisor) { + None => Err(format!( + "Attempted to divide by zero while validating {}. \ + (This is likely due to an internal bug.)", + field_name, + )), + + Some(0) => Ok(dividend.saturating_div(divisor)), + + Some(remainder) => { + assert_ne!(remainder, 0); + Err(format!( + "{} is supposed to contain a value that is evenly divisible by {}, \ + but it contains {}, which leaves a remainder of {}.", + field_name, divisor, dividend, remainder, + )) + } + } +} + +impl TryFrom for SnsInitPayload { + type Error = String; + + fn try_from(src: CreateServiceNervousSystem) -> Result { + let CreateServiceNervousSystem { + name, + description, + url, + logo, + fallback_controller_principal_ids, + dapp_canisters, + + initial_token_distribution, + + swap_parameters, + ledger_parameters, + governance_parameters, + } = src; + + let mut defects = vec![]; + + let ledger_parameters = ledger_parameters.unwrap_or_default(); + let governance_parameters = governance_parameters.unwrap_or_default(); + let swap_parameters = swap_parameters.unwrap_or_default(); + + let create_sns::LedgerParameters { + transaction_fee, + token_name, + token_symbol, + token_logo, + } = ledger_parameters; + + let transaction_fee_e8s = transaction_fee.and_then(|tokens| tokens.e8s); + + let token_logo = token_logo.and_then(|image| image.base64_encoding); + + let proposal_reject_cost_e8s = governance_parameters + .proposal_rejection_fee + .and_then(|tokens| tokens.e8s); + + let neuron_minimum_stake_e8s = governance_parameters + .neuron_minimum_stake + .and_then(|tokens| tokens.e8s); + + let initial_token_distribution = match sns_init_payload::InitialTokenDistribution::try_from( + initial_token_distribution.unwrap_or_default(), + ) { + Ok(ok) => Some(ok), + Err(err) => { + defects.push(err); + None + } + }; + + let fallback_controller_principal_ids = fallback_controller_principal_ids + .iter() + .map(|principal_id| principal_id.to_string()) + .collect(); + + let logo = logo.and_then(|image| image.base64_encoding); + // url, name, and description need no conversion. + + let neuron_minimum_dissolve_delay_to_vote_seconds = governance_parameters + .neuron_minimum_dissolve_delay_to_vote + .and_then(|duration| duration.seconds); + + let voting_reward_parameters = governance_parameters + .voting_reward_parameters + .unwrap_or_default(); + + let initial_reward_rate_basis_points = voting_reward_parameters + .initial_reward_rate + .and_then(|percentage| percentage.basis_points); + let final_reward_rate_basis_points = voting_reward_parameters + .final_reward_rate + .and_then(|percentage| percentage.basis_points); + + let reward_rate_transition_duration_seconds = voting_reward_parameters + .reward_rate_transition_duration + .and_then(|duration| duration.seconds); + + let max_dissolve_delay_seconds = governance_parameters + .neuron_maximum_dissolve_delay + .and_then(|duration| duration.seconds); + + let max_neuron_age_seconds_for_age_bonus = governance_parameters + .neuron_maximum_age_for_age_bonus + .and_then(|duration| duration.seconds); + + let mut basis_points_to_percentage = |field_name, percentage: nns_pb::Percentage| -> u64 { + let basis_points = percentage.basis_points.unwrap_or_default(); + match divide_perfectly(field_name, basis_points, 100) { + Ok(ok) => ok, + Err(err) => { + defects.push(err); + basis_points.saturating_div(100) + } + } + }; + + let max_dissolve_delay_bonus_percentage = governance_parameters + .neuron_maximum_dissolve_delay_bonus + .map(|percentage| { + basis_points_to_percentage( + "governance_parameters.neuron_maximum_dissolve_delay_bonus", + percentage, + ) + }); + + let max_age_bonus_percentage = + governance_parameters + .neuron_maximum_age_bonus + .map(|percentage| { + basis_points_to_percentage( + "governance_parameters.neuron_maximum_age_bonus", + percentage, + ) + }); + + let initial_voting_period_seconds = governance_parameters + .proposal_initial_voting_period + .and_then(|duration| duration.seconds); + + let wait_for_quiet_deadline_increase_seconds = governance_parameters + .proposal_wait_for_quiet_deadline_increase + .and_then(|duration| duration.seconds); + + let dapp_canisters = Some(sns_pb::DappCanisters { + canisters: dapp_canisters, + }); + + let confirmation_text = swap_parameters.confirmation_text; + + let restricted_countries = swap_parameters.restricted_countries; + + let min_participants = swap_parameters.minimum_participants; + + let min_direct_participation_icp_e8s = swap_parameters + .minimum_direct_participation_icp + .and_then(|tokens| tokens.e8s); + + let max_direct_participation_icp_e8s = swap_parameters + .maximum_direct_participation_icp + .and_then(|tokens| tokens.e8s); + + // Check if the deprecated fields are set. + if let Some(neurons_fund_investment_icp) = swap_parameters.neurons_fund_investment_icp { + defects.push(format!( + "neurons_fund_investment_icp ({:?}) is deprecated; please set \ + neurons_fund_participation instead.", + neurons_fund_investment_icp, + )); + } + if let Some(minimum_icp) = swap_parameters.minimum_icp { + defects.push(format!( + "minimum_icp ({:?}) is deprecated; please set \ + min_direct_participation_icp_e8s instead.", + minimum_icp, + )); + }; + if let Some(maximum_icp) = swap_parameters.maximum_icp { + defects.push(format!( + "maximum_icp ({:?}) is deprecated; please set \ + max_direct_participation_icp_e8s instead.", + maximum_icp, + )); + }; + + let neurons_fund_participation = swap_parameters.neurons_fund_participation; + + let min_participant_icp_e8s = swap_parameters + .minimum_participant_icp + .and_then(|tokens| tokens.e8s); + + let max_participant_icp_e8s = swap_parameters + .maximum_participant_icp + .and_then(|tokens| tokens.e8s); + + let neuron_basket_construction_parameters = swap_parameters + .neuron_basket_construction_parameters + .map(|basket| sns_swap_pb::NeuronBasketConstructionParameters { + count: basket.count.unwrap_or_default(), + dissolve_delay_interval_seconds: basket + .dissolve_delay_interval + .map(|duration| duration.seconds.unwrap_or_default()) + .unwrap_or_default(), + }); + + if !defects.is_empty() { + return Err(format!( + "Failed to convert CreateServiceNervousSystem proposal to SnsInitPayload:\n{}", + defects.join("\n"), + )); + } + + let result = Self { + transaction_fee_e8s, + token_name, + token_symbol, + proposal_reject_cost_e8s, + neuron_minimum_stake_e8s, + initial_token_distribution, + fallback_controller_principal_ids, + logo, + url, + name, + description, + neuron_minimum_dissolve_delay_to_vote_seconds, + initial_reward_rate_basis_points, + final_reward_rate_basis_points, + reward_rate_transition_duration_seconds, + max_dissolve_delay_seconds, + max_neuron_age_seconds_for_age_bonus, + max_dissolve_delay_bonus_percentage, + max_age_bonus_percentage, + initial_voting_period_seconds, + wait_for_quiet_deadline_increase_seconds, + dapp_canisters, + min_participants, + min_direct_participation_icp_e8s, + max_direct_participation_icp_e8s, + min_participant_icp_e8s, + max_participant_icp_e8s, + neuron_basket_construction_parameters, + confirmation_text, + restricted_countries, + token_logo, + neurons_fund_participation, + + // These are not known from only the CreateServiceNervousSystem + // proposal. See TryFrom + nns_proposal_id: None, + neurons_fund_participants: None, + swap_start_timestamp_seconds: None, + swap_due_timestamp_seconds: None, + neurons_fund_participation_constraints: None, + + // Deprecated fields should be set to `None`. + min_icp_e8s: None, + max_icp_e8s: None, + }; + + result.validate_pre_execution()?; + + Ok(result) + } +} + +impl TryFrom for sns_init_payload::InitialTokenDistribution { + type Error = String; + + fn try_from(src: create_sns::InitialTokenDistribution) -> Result { + let create_sns::InitialTokenDistribution { + developer_distribution, + treasury_distribution, + swap_distribution, + } = src; + + let mut defects = vec![]; + + let developer_distribution = match sns_pb::DeveloperDistribution::try_from( + developer_distribution.unwrap_or_default(), + ) { + Ok(ok) => Some(ok), + Err(err) => { + defects.push(err); + None + } + }; + + let treasury_distribution = + match sns_pb::TreasuryDistribution::try_from(treasury_distribution.unwrap_or_default()) + { + Ok(ok) => Some(ok), + Err(err) => { + defects.push(err); + None + } + }; + + let swap_distribution = + match sns_pb::SwapDistribution::try_from(swap_distribution.unwrap_or_default()) { + Ok(ok) => Some(ok), + Err(err) => { + defects.push(err); + None + } + }; + + let airdrop_distribution = Some(sns_pb::AirdropDistribution::default()); + + if !defects.is_empty() { + return Err(format!( + "Failed to convert to InitialTokenDistribution for the following reasons:\n{}", + defects.join("\n"), + )); + } + + Ok(Self::FractionalDeveloperVotingPower( + sns_pb::FractionalDeveloperVotingPower { + developer_distribution, + treasury_distribution, + swap_distribution, + airdrop_distribution, + }, + )) + } +} + +impl TryFrom + for sns_pb::SwapDistribution +{ + type Error = String; + + fn try_from( + src: create_sns::initial_token_distribution::SwapDistribution, + ) -> Result { + let create_sns::initial_token_distribution::SwapDistribution { total } = src; + + let total_e8s = total.unwrap_or_default().e8s.unwrap_or_default(); + let initial_swap_amount_e8s = total_e8s; + + Ok(Self { + initial_swap_amount_e8s, + total_e8s, + }) + } +} + +impl TryFrom + for sns_pb::TreasuryDistribution +{ + type Error = String; + + fn try_from( + src: create_sns::initial_token_distribution::TreasuryDistribution, + ) -> Result { + let create_sns::initial_token_distribution::TreasuryDistribution { total } = src; + + let total_e8s = total.unwrap_or_default().e8s.unwrap_or_default(); + + Ok(Self { total_e8s }) + } +} + +impl TryFrom + for sns_pb::DeveloperDistribution +{ + type Error = String; + + fn try_from( + src: create_sns::initial_token_distribution::DeveloperDistribution, + ) -> Result { + let create_sns::initial_token_distribution::DeveloperDistribution { developer_neurons } = + src; + + let mut defects = vec![]; + + let developer_neurons = developer_neurons + .into_iter() + .enumerate() + .filter_map(|(i, neuron_distribution)| { + match sns_pb::NeuronDistribution::try_from(neuron_distribution) { + Ok(ok) => Some(ok), + Err(err) => { + defects.push(format!( + "Failed to convert element at index {} in field \ + `developer_neurons`: {}", + i, err, + )); + None + } + } + }) + .collect(); + + if !defects.is_empty() { + return Err(format!( + "Failed to convert to DeveloperDistribution for SnsInitPayload: {}", + defects.join("\n"), + )); + } + + Ok(Self { developer_neurons }) + } +} + +impl TryFrom + for sns_pb::NeuronDistribution +{ + type Error = String; + + fn try_from( + src: create_sns::initial_token_distribution::developer_distribution::NeuronDistribution, + ) -> Result { + let create_sns::initial_token_distribution::developer_distribution::NeuronDistribution { + controller, + dissolve_delay, + memo, + stake, + vesting_period, + } = src; + + // controller needs no conversion + let stake_e8s = stake.unwrap_or_default().e8s.unwrap_or_default(); + let memo = memo.unwrap_or_default(); + let dissolve_delay_seconds = dissolve_delay + .unwrap_or_default() + .seconds + .unwrap_or_default(); + let vesting_period_seconds = vesting_period.unwrap_or_default().seconds; + + Ok(Self { + controller, + stake_e8s, + memo, + dissolve_delay_seconds, + vesting_period_seconds, + }) + } +} + +#[derive(Clone)] +pub struct ExecutedCreateServiceNervousSystemProposal { + pub current_timestamp_seconds: u64, + pub create_service_nervous_system: CreateServiceNervousSystem, + pub proposal_id: u64, + pub random_swap_start_time: GlobalTimeOfDay, + /// Information about the Neurons' Fund participation needed by the Swap canister. + pub neurons_fund_participation_constraints: Option, +} + +impl TryFrom for SnsInitPayload { + type Error = String; + + fn try_from(src: ExecutedCreateServiceNervousSystemProposal) -> Result { + let mut defects = vec![]; + + let current_timestamp_seconds = src.current_timestamp_seconds; + let nns_proposal_id = Some(src.proposal_id); + let neurons_fund_participation_constraints = src.neurons_fund_participation_constraints; + let start_time = src + .create_service_nervous_system + .swap_parameters + .as_ref() + .and_then(|swap_parameters| swap_parameters.start_time); + let duration = src + .create_service_nervous_system + .swap_parameters + .as_ref() + .and_then(|swap_parameters| swap_parameters.duration); + + let (swap_start_timestamp_seconds, swap_due_timestamp_seconds) = + match CreateServiceNervousSystem::swap_start_and_due_timestamps( + start_time.unwrap_or(src.random_swap_start_time), + duration.unwrap_or_default(), + current_timestamp_seconds, + ) { + Ok((swap_start_timestamp_seconds, swap_due_timestamp_seconds)) => ( + Some(swap_start_timestamp_seconds), + Some(swap_due_timestamp_seconds), + ), + Err(err) => { + defects.push(err); + (None, None) + } + }; + + let mut result = SnsInitPayload::try_from(src.create_service_nervous_system)?; + + result.nns_proposal_id = nns_proposal_id; + result.swap_start_timestamp_seconds = swap_start_timestamp_seconds; + result.swap_due_timestamp_seconds = swap_due_timestamp_seconds; + result.neurons_fund_participation_constraints = neurons_fund_participation_constraints; + + result.validate_post_execution()?; + + Ok(result) + } +} + +impl CreateServiceNervousSystem { + pub fn sns_token_e8s(&self) -> Option { + self.initial_token_distribution + .as_ref()? + .swap_distribution + .as_ref()? + .total + .as_ref()? + .e8s + } + + pub fn transaction_fee_e8s(&self) -> Option { + self.ledger_parameters + .as_ref()? + .transaction_fee + .as_ref()? + .e8s + } + + pub fn neuron_minimum_stake_e8s(&self) -> Option { + self.governance_parameters + .as_ref()? + .neuron_minimum_stake + .as_ref()? + .e8s + } + + /// Computes timestamps for when the SNS token swap will start, and will be + /// due, based on the start and end times. + /// + /// The swap will start on the first `start_time_of_day` that is more than + /// 24h after the swap was approved. + /// + /// The end time is calculated by adding `duration` to the computed start time. + /// + /// if start_time_of_day is None, then randomly_pick_swap_start is used to + /// pick a start time. + pub fn swap_start_and_due_timestamps( + start_time_of_day: GlobalTimeOfDay, + duration: Duration, + swap_approved_timestamp_seconds: u64, + ) -> Result<(u64, u64), String> { + let start_time_of_day = start_time_of_day + .seconds_after_utc_midnight + .ok_or("`seconds_after_utc_midnight` should not be None")?; + let duration = duration.seconds.ok_or("`seconds` should not be None")?; + + // TODO(NNS1-2298): we should also add 27 leap seconds to this, to avoid + // having the swap start half a minute earlier than expected. + let midnight_after_swap_approved_timestamp_seconds = swap_approved_timestamp_seconds + .saturating_sub(swap_approved_timestamp_seconds % ONE_DAY_SECONDS) // floor to midnight + .saturating_add(ONE_DAY_SECONDS); // add one day + + let swap_start_timestamp_seconds = { + let mut possible_swap_starts = (0..2).map(|i| { + midnight_after_swap_approved_timestamp_seconds + .saturating_add(ONE_DAY_SECONDS * i) + .saturating_add(start_time_of_day) + }); + // Find the earliest time that's at least 24h after the swap was approved. + possible_swap_starts + .find(|×tamp| timestamp > swap_approved_timestamp_seconds + ONE_DAY_SECONDS) + .ok_or(format!( + "Unable to find a swap start time after the swap was approved. \ + swap_approved_timestamp_seconds = {}, \ + midnight_after_swap_approved_timestamp_seconds = {}, \ + start_time_of_day = {}, \ + duration = {} \ + This is probably a bug.", + swap_approved_timestamp_seconds, + midnight_after_swap_approved_timestamp_seconds, + start_time_of_day, + duration, + ))? + }; + + let swap_due_timestamp_seconds = duration + .checked_add(swap_start_timestamp_seconds) + .ok_or("`duration` should not be None")?; + + Ok((swap_start_timestamp_seconds, swap_due_timestamp_seconds)) + } +} diff --git a/sns-validation/src/pbs/nns_pb.rs b/sns-validation/src/pbs/nns_pb.rs new file mode 100644 index 00000000..20587239 --- /dev/null +++ b/sns-validation/src/pbs/nns_pb.rs @@ -0,0 +1,120 @@ +// From https://github.com/dfinity/ic/blob/master/rs/nervous_system/proto/src/gen/ic_nervous_system.pb.v1.rs + +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize, Copy)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Duration { + pub seconds: ::core::option::Option, +} +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize, Copy)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct GlobalTimeOfDay { + pub seconds_after_utc_midnight: ::core::option::Option, +} +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize, Copy)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Tokens { + pub e8s: ::core::option::Option, +} +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Image { + /// A data URI of a png. E.g. + /// data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAD0lEQVQIHQEEAPv/AAD/DwIRAQ8HgT3GAAAAAElFTkSuQmCC + /// ^ 1 pixel containing the color #00FF0F. + pub base64_encoding: ::core::option::Option, +} +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize, Copy, PartialOrd, Ord)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Percentage { + pub basis_points: ::core::option::Option, +} +/// A list of principals. +/// Needed to allow prost to generate the equivalent of Optional>. +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Principals { + pub principals: Vec, +} +/// A Canister that will be transferred to an SNS. +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize, Ord, PartialOrd, Copy)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Canister { + /// The id of the canister. + pub id: ::core::option::Option, +} +/// Represents a set of countries. To be used in country-specific configurations, +/// e.g., to restrict the geography of an SNS swap. +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Countries { + /// ISO 3166-1 alpha-2 codes + pub iso_codes: Vec, +} +/// Features: +/// 1. Sign ('+' is optional). +/// 2. Smallest positive value: 10^-28. +/// 3. 96 bits of significand. +/// 4. Decimal point character: '.' (dot/period). +#[derive(Eq, candid::CandidType, candid::Deserialize, serde::Serialize)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug, Default)] +pub struct Decimal { + /// E.g. "3.14". + pub human_readable: ::core::option::Option, +} + +impl GlobalTimeOfDay { + pub fn from_hh_mm(hh: u64, mm: u64) -> Result { + if hh >= 23 || mm >= 60 { + return Err(format!("invalid time of day ({}:{})", hh, mm)); + } + let seconds_after_utc_midnight = Some(hh * 3600 + mm * 60); + Ok(Self { + seconds_after_utc_midnight, + }) + } + + pub fn as_hh_mm(&self) -> Option<(u64, u64)> { + let hh = self.seconds_after_utc_midnight? / 3600; + let mm = (self.seconds_after_utc_midnight? % 3600) / 60; + Some((hh, mm)) + } +} + +impl Tokens { + pub fn checked_add(&self, rhs: &Tokens) -> Option { + let e8s = self.e8s?.checked_add(rhs.e8s?)?; + Some(Tokens { e8s: Some(e8s) }) + } + + pub fn checked_sub(&self, rhs: &Tokens) -> Option { + let e8s = self.e8s?.checked_sub(rhs.e8s?)?; + Some(Tokens { e8s: Some(e8s) }) + } +} + +impl Percentage { + pub fn from_percentage(percentage: f64) -> Percentage { + assert!( + !percentage.is_sign_negative(), + "percentage must be non-negative" + ); + Percentage { + basis_points: Some((percentage * 100.0).round() as u64), + } + } + + pub const fn from_basis_points(basis_points: u64) -> Percentage { + Percentage { + basis_points: Some(basis_points), + } + } +} diff --git a/sns-validation/src/pbs/sns_pb.rs b/sns-validation/src/pbs/sns_pb.rs new file mode 100644 index 00000000..77f96cfb --- /dev/null +++ b/sns-validation/src/pbs/sns_pb.rs @@ -0,0 +1,165 @@ +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct SnsInitPayload { + pub transaction_fee_e8s: ::core::option::Option, + + pub token_name: ::core::option::Option, + + pub token_symbol: ::core::option::Option, + + pub proposal_reject_cost_e8s: ::core::option::Option, + + pub neuron_minimum_stake_e8s: ::core::option::Option, + + pub fallback_controller_principal_ids: Vec, + + pub logo: ::core::option::Option, + + pub url: ::core::option::Option, + + pub name: ::core::option::Option, + + pub description: ::core::option::Option, + + pub neuron_minimum_dissolve_delay_to_vote_seconds: ::core::option::Option, + + pub initial_reward_rate_basis_points: ::core::option::Option, + pub final_reward_rate_basis_points: ::core::option::Option, + + pub reward_rate_transition_duration_seconds: ::core::option::Option, + + pub max_dissolve_delay_seconds: ::core::option::Option, + + pub max_neuron_age_seconds_for_age_bonus: ::core::option::Option, + + pub max_dissolve_delay_bonus_percentage: ::core::option::Option, + + pub max_age_bonus_percentage: ::core::option::Option, + + pub initial_voting_period_seconds: ::core::option::Option, + + pub wait_for_quiet_deadline_increase_seconds: ::core::option::Option, + + pub confirmation_text: ::core::option::Option, + + pub restricted_countries: ::core::option::Option, + + pub dapp_canisters: ::core::option::Option, + + pub min_participants: ::core::option::Option, + + pub min_icp_e8s: ::core::option::Option, + + pub max_icp_e8s: ::core::option::Option, + + pub min_direct_participation_icp_e8s: ::core::option::Option, + + pub max_direct_participation_icp_e8s: ::core::option::Option, + + pub min_participant_icp_e8s: ::core::option::Option, + + pub max_participant_icp_e8s: ::core::option::Option, + + pub swap_start_timestamp_seconds: ::core::option::Option, + + pub swap_due_timestamp_seconds: ::core::option::Option, + + pub neuron_basket_construction_parameters: + ::core::option::Option, + + pub nns_proposal_id: ::core::option::Option, + + pub neurons_fund_participation: ::core::option::Option, + + pub neurons_fund_participants: ::core::option::Option, + + pub token_logo: ::core::option::Option, + + pub neurons_fund_participation_constraints: + ::core::option::Option, + + pub initial_token_distribution: + ::core::option::Option, +} + +pub mod sns_init_payload { + + #[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] + #[allow(clippy::derive_partial_eq_without_eq)] + #[derive(Clone, PartialEq, Debug)] + pub enum InitialTokenDistribution { + FractionalDeveloperVotingPower(super::FractionalDeveloperVotingPower), + } +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct FractionalDeveloperVotingPower { + pub developer_distribution: ::core::option::Option, + + pub treasury_distribution: ::core::option::Option, + + pub swap_distribution: ::core::option::Option, + + pub airdrop_distribution: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct DeveloperDistribution { + pub developer_neurons: Vec, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct TreasuryDistribution { + pub total_e8s: u64, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct SwapDistribution { + pub total_e8s: u64, + + pub initial_swap_amount_e8s: u64, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Default, Debug)] +pub struct AirdropDistribution { + pub airdrop_neurons: Vec, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct NeuronDistribution { + pub controller: ::core::option::Option, + + pub stake_e8s: u64, + + pub memo: u64, + + pub dissolve_delay_seconds: u64, + + pub vesting_period_seconds: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct DappCanisters { + pub canisters: Vec, +} +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct NeuronsFundParticipants { + pub participants: Vec, +} diff --git a/sns-validation/src/pbs/sns_swap_pb.rs b/sns-validation/src/pbs/sns_swap_pb.rs new file mode 100644 index 00000000..ddef5b57 --- /dev/null +++ b/sns-validation/src/pbs/sns_swap_pb.rs @@ -0,0 +1,69 @@ +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct CfParticipant { + pub controller: ::core::option::Option, + + pub cf_neurons: Vec, + + #[deprecated] + pub hotkey_principal: String, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct CfNeuron { + pub nns_neuron_id: u64, + + pub amount_icp_e8s: u64, + + pub hotkeys: ::core::option::Option, + + pub has_created_neuron_recipes: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct NeuronBasketConstructionParameters { + pub count: u64, + + pub dissolve_delay_interval_seconds: u64, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct NeuronsFundParticipationConstraints { + pub min_direct_participation_threshold_icp_e8s: ::core::option::Option, + + pub max_neurons_fund_participation_icp_e8s: ::core::option::Option, + + pub coefficient_intervals: Vec, + + pub ideal_matched_participation_function: + ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct IdealMatchedParticipationFunction { + pub serialized_representation: ::core::option::Option, +} + +#[derive(candid::CandidType, candid::Deserialize, serde::Serialize, Eq)] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, Debug)] +pub struct LinearScalingCoefficient { + pub from_direct_participation_icp_e8s: ::core::option::Option, + + pub to_direct_participation_icp_e8s: ::core::option::Option, + + pub slope_numerator: ::core::option::Option, + + pub slope_denominator: ::core::option::Option, + + pub intercept_icp_e8s: ::core::option::Option, +} diff --git a/sns-validation/src/validation/mod.rs b/sns-validation/src/validation/mod.rs new file mode 100644 index 00000000..66af956e --- /dev/null +++ b/sns-validation/src/validation/mod.rs @@ -0,0 +1,3 @@ +pub mod neurons_fund; +pub mod sns_gov; +pub mod sns_init; diff --git a/sns-validation/src/validation/neurons_fund.rs b/sns-validation/src/validation/neurons_fund.rs new file mode 100644 index 00000000..a8b5db86 --- /dev/null +++ b/sns-validation/src/validation/neurons_fund.rs @@ -0,0 +1,383 @@ +use crate::pbs::sns_swap_pb::{LinearScalingCoefficient, NeuronsFundParticipationConstraints}; + +// The maximum number of bytes that a serialized representation of an ideal matching function +// `IdealMatchedParticipationFunction` may have. +pub const MAX_MATCHING_FUNCTION_SERIALIZED_REPRESENTATION_SIZE_BYTES: usize = 1_000; + +// The maximum number of intervals for scaling ideal Neurons' Fund participation down to effective +// participation. Theoretically, this number should be greater than double the number of neurons +// participating in the Neurons' Fund. Although the currently chosen value is quite high, it is +// still significantly smaller than `usize::MAX`, allowing to reject an misformed +// SnsInitPayload.coefficient_intervals structure with obviously too many elements. +pub const MAX_LINEAR_SCALING_COEFFICIENT_VEC_LEN: usize = 100_000; + +#[derive(Debug)] +pub enum LinearScalingCoefficientValidationError { + // All fields are mandatory. + UnspecifiedField(String), + EmptyInterval { + from_direct_participation_icp_e8s: u64, + to_direct_participation_icp_e8s: u64, + }, + DenominatorIsZero, + // The slope should be between 0.0 and 1.0. + NumeratorGreaterThanDenominator { + slope_numerator: u64, + slope_denominator: u64, + }, +} + +impl std::fmt::Display for LinearScalingCoefficientValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "LinearScalingCoefficientValidationError: "; + match self { + Self::UnspecifiedField(field_name) => { + write!(f, "{prefix}Field `{}` must be specified.", field_name) + } + Self::EmptyInterval { + from_direct_participation_icp_e8s, + to_direct_participation_icp_e8s, + } => { + write!( + f, + "{prefix}from_direct_participation_icp_e8s ({}) must be strictly less that \ + to_direct_participation_icp_e8s ({})).", + from_direct_participation_icp_e8s, to_direct_participation_icp_e8s, + ) + } + Self::DenominatorIsZero => { + write!(f, "{prefix}slope_denominator must not equal zero.") + } + Self::NumeratorGreaterThanDenominator { + slope_numerator, + slope_denominator, + } => { + write!( + f, + "{prefix}slope_numerator ({}) must be less than or equal \ + slope_denominator ({})", + slope_numerator, slope_denominator, + ) + } + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ValidatedLinearScalingCoefficient { + pub from_direct_participation_icp_e8s: u64, + pub to_direct_participation_icp_e8s: u64, + pub slope_numerator: u64, + pub slope_denominator: u64, + pub intercept_icp_e8s: u64, +} + +#[derive(Debug)] +pub enum LinearScalingCoefficientVecValidationError { + LinearScalingCoefficientsOutOfRange(usize), + LinearScalingCoefficientsUnordered( + ValidatedLinearScalingCoefficient, + ValidatedLinearScalingCoefficient, + ), + IrregularLinearScalingCoefficients(ValidatedLinearScalingCoefficient), + LinearScalingCoefficientValidationError(LinearScalingCoefficientValidationError), +} + +impl std::fmt::Display for LinearScalingCoefficientVecValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "LinearScalingCoefficientVecValidationError: "; + match self { + Self::LinearScalingCoefficientsOutOfRange(num_elements) => { + write!( + f, + "{}coefficient_intervals (len={}) must contain at least 1 and at most {} elements.", + prefix, num_elements, MAX_LINEAR_SCALING_COEFFICIENT_VEC_LEN, + ) + } + Self::LinearScalingCoefficientsUnordered(left, right) => { + write!( + f, + "{}The intervals {:?} and {:?} are ordered incorrectly.", + prefix, left, right + ) + } + Self::IrregularLinearScalingCoefficients(interval) => { + write!( + f, + "{}The first interval {:?} does not start from 0.", + prefix, interval, + ) + } + Self::LinearScalingCoefficientValidationError(error) => { + write!(f, "{}{}", prefix, error) + } + } + } +} + +impl From for Result<(), String> { + fn from(value: LinearScalingCoefficientVecValidationError) -> Self { + Err(value.to_string()) + } +} + +#[derive(Debug)] +pub enum IdealMatchedParticipationFunctionValidationError { + TooManyBytes(usize), + DeserializationError { + /// Value that could not be deserialized. + input: String, + /// Why deserialization did not work. + err: String, + }, +} + +impl std::fmt::Display for IdealMatchedParticipationFunctionValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "IdealMatchedParticipationFunctionValidationError: "; + match self { + Self::TooManyBytes(num_bytes) => { + write!( + f, + "{prefix} serialized representation has {} bytes; the maximum is {} bytes.", + num_bytes, MAX_MATCHING_FUNCTION_SERIALIZED_REPRESENTATION_SIZE_BYTES, + ) + } + Self::DeserializationError { input, err } => { + write!( + f, + "{prefix} deserialization failed: {}; input: `{}`.", + err, input + ) + } + } + } +} + +#[derive(Debug)] +pub enum NeuronsFundParticipationConstraintsValidationError { + RelatedFieldUnspecified(String), + LinearScalingCoefficientVecValidationError(LinearScalingCoefficientVecValidationError), + IdealMatchedParticipationFunctionValidationError( + IdealMatchedParticipationFunctionValidationError, + ), +} + +impl std::fmt::Display for NeuronsFundParticipationConstraintsValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "NeuronsFundParticipationConstraintsValidationError: "; + match self { + Self::RelatedFieldUnspecified(related_field_name) => { + write!(f, "{}{} must be specified.", prefix, related_field_name,) + } + Self::LinearScalingCoefficientVecValidationError(error) => { + write!(f, "{}{}", prefix, error) + } + Self::IdealMatchedParticipationFunctionValidationError(error) => { + write!(f, "{prefix}{}", error) + } + } + } +} + +impl From for Result<(), String> { + fn from(value: NeuronsFundParticipationConstraintsValidationError) -> Self { + Err(value.to_string()) + } +} + +#[derive(Clone, Debug)] +pub struct ValidatedNeuronsFundParticipationConstraints { + // pub min_direct_participation_threshold_icp_e8s: u64, + // pub max_neurons_fund_participation_icp_e8s: u64, + // pub coefficient_intervals: Vec, + // pub ideal_matched_participation_function: Box, +} + +impl NeuronsFundParticipationConstraints { + /// Make the validation function available to crates that do not import + /// `ValidatedNeuronsFundParticipationConstraints` directly, e.g., `rs/sns/init`. + pub fn validate(&self) -> Result<(), NeuronsFundParticipationConstraintsValidationError> { + ValidatedNeuronsFundParticipationConstraints::try_from(self).map(|_| ()) + } +} + +impl TryFrom<&NeuronsFundParticipationConstraints> + for ValidatedNeuronsFundParticipationConstraints +{ + type Error = NeuronsFundParticipationConstraintsValidationError; + + fn try_from(value: &NeuronsFundParticipationConstraints) -> Result { + // Validate min_direct_participation_threshold_icp_e8s + let _min_direct_participation_threshold_icp_e8s = value + .min_direct_participation_threshold_icp_e8s + .ok_or_else(|| { + Self::Error::RelatedFieldUnspecified( + "min_direct_participation_threshold_icp_e8s".to_string(), + ) + })?; + + // Validate max_neurons_fund_participation_icp_e8s + let _max_neurons_fund_participation_icp_e8s = value + .max_neurons_fund_participation_icp_e8s + .ok_or_else(|| { + Self::Error::RelatedFieldUnspecified( + "max_neurons_fund_participation_icp_e8s".to_string(), + ) + })?; + + // Validate coefficient_intervals length. + if !(1..MAX_LINEAR_SCALING_COEFFICIENT_VEC_LEN + 1) + .contains(&value.coefficient_intervals.len()) + { + return Err(Self::Error::LinearScalingCoefficientVecValidationError( + LinearScalingCoefficientVecValidationError::LinearScalingCoefficientsOutOfRange( + value.coefficient_intervals.len(), + ), + )); + } + + // Validate individual coefficient_intervals elements, consuming value. + let coefficient_intervals: Vec = value + .coefficient_intervals + .iter() + .map(ValidatedLinearScalingCoefficient::try_from) + .collect::, _>>() + .map_err(|err| { + Self::Error::LinearScalingCoefficientVecValidationError( + LinearScalingCoefficientVecValidationError::LinearScalingCoefficientValidationError(err) + ) + })?; + + // Validate that coefficient_intervals forms a partitioning. + let intervals = &coefficient_intervals; + intervals + .iter() + .zip(intervals.iter().skip(1)) + .find(|(prev, this)| { + prev.to_direct_participation_icp_e8s != this.from_direct_participation_icp_e8s + }) + .map_or(Ok(()), |(prev, this)| { + Err(Self::Error::LinearScalingCoefficientVecValidationError( + LinearScalingCoefficientVecValidationError::LinearScalingCoefficientsUnordered( + prev.clone(), + this.clone(), + ), + )) + })?; + + // Validate that coefficient_intervals starts from 0. + if let Some(first_interval) = intervals.first() { + if first_interval.from_direct_participation_icp_e8s != 0 { + return Err(Self::Error::LinearScalingCoefficientVecValidationError( + LinearScalingCoefficientVecValidationError::IrregularLinearScalingCoefficients( + first_interval.clone(), + ), + )); + } + } + + let matching_function_serialized_representation = value + .ideal_matched_participation_function + .as_ref() + .ok_or_else(|| { + Self::Error::RelatedFieldUnspecified( + "ideal_matched_participation_function".to_string(), + ) + })? + .serialized_representation + .as_ref() + .ok_or_else(|| { + Self::Error::RelatedFieldUnspecified( + "ideal_matched_participation_function.serialized_representation".to_string(), + ) + })?; + if matching_function_serialized_representation.len() + > MAX_MATCHING_FUNCTION_SERIALIZED_REPRESENTATION_SIZE_BYTES + { + return Err( + Self::Error::IdealMatchedParticipationFunctionValidationError( + IdealMatchedParticipationFunctionValidationError::TooManyBytes( + matching_function_serialized_representation.len(), + ), + ), + ); + } + + // TODO: add proper validation here + // let ideal_matched_participation_function = + // F::from_repr(matching_function_serialized_representation).map_err(|err| { + // Self::Error::IdealMatchedParticipationFunctionValidationError( + // IdealMatchedParticipationFunctionValidationError::DeserializationError { + // input: matching_function_serialized_representation.clone(), + // err, + // }, + // ) + // })?; + + Ok(Self { + // min_direct_participation_threshold_icp_e8s, + // max_neurons_fund_participation_icp_e8s, + // coefficient_intervals, + // ideal_matched_participation_function, + }) + } +} + +impl TryFrom<&LinearScalingCoefficient> for ValidatedLinearScalingCoefficient { + type Error = LinearScalingCoefficientValidationError; + + fn try_from(value: &LinearScalingCoefficient) -> Result { + let from_direct_participation_icp_e8s = + value.from_direct_participation_icp_e8s.ok_or_else(|| { + LinearScalingCoefficientValidationError::UnspecifiedField( + "from_direct_participation_icp_e8s".to_string(), + ) + })?; + let to_direct_participation_icp_e8s = + value.to_direct_participation_icp_e8s.ok_or_else(|| { + LinearScalingCoefficientValidationError::UnspecifiedField( + "to_direct_participation_icp_e8s".to_string(), + ) + })?; + let slope_numerator = value.slope_numerator.ok_or_else(|| { + LinearScalingCoefficientValidationError::UnspecifiedField("slope_numerator".to_string()) + })?; + let slope_denominator = value.slope_denominator.ok_or_else(|| { + LinearScalingCoefficientValidationError::UnspecifiedField( + "slope_denominator".to_string(), + ) + })?; + // Currently we only check that `intercept_icp_e8s` is specified, so the actual field value + // is unchecked. + let intercept_icp_e8s = value.intercept_icp_e8s.ok_or_else(|| { + LinearScalingCoefficientValidationError::UnspecifiedField( + "intercept_icp_e8s".to_string(), + ) + })?; + if to_direct_participation_icp_e8s <= from_direct_participation_icp_e8s { + return Err(LinearScalingCoefficientValidationError::EmptyInterval { + from_direct_participation_icp_e8s, + to_direct_participation_icp_e8s, + }); + } + if slope_denominator == 0 { + return Err(LinearScalingCoefficientValidationError::DenominatorIsZero); + } + if slope_numerator > slope_denominator { + return Err( + LinearScalingCoefficientValidationError::NumeratorGreaterThanDenominator { + slope_numerator, + slope_denominator, + }, + ); + } + Ok(Self { + from_direct_participation_icp_e8s, + to_direct_participation_icp_e8s, + slope_numerator, + slope_denominator, + intercept_icp_e8s, + }) + } +} diff --git a/sns-validation/src/validation/sns_gov.rs b/sns-validation/src/validation/sns_gov.rs new file mode 100644 index 00000000..49d6cd82 --- /dev/null +++ b/sns-validation/src/validation/sns_gov.rs @@ -0,0 +1,90 @@ +use crate::{ + consts::{E8S_PER_TOKEN, ONE_DAY_SECONDS, ONE_MONTH_SECONDS, ONE_YEAR_SECONDS}, + pbs::{ + gov_pb::{ + DefaultFollowees, NervousSystemParameters, NeuronPermissionList, + VotingRewardsParameters, + }, + nns_pb::Percentage, + }, +}; + +impl VotingRewardsParameters { + pub const INITIAL_REWARD_RATE_BASIS_POINTS_CEILING: u64 = 10_000; + + pub fn with_default_values() -> Self { + Self { + round_duration_seconds: Some(ONE_DAY_SECONDS), + reward_rate_transition_duration_seconds: Some(0), + initial_reward_rate_basis_points: Some(0), + final_reward_rate_basis_points: Some(0), + } + } +} + +impl NervousSystemParameters { + pub const MAX_PROPOSALS_TO_KEEP_PER_ACTION_CEILING: u32 = 700; + + pub const MAX_NUMBER_OF_NEURONS_CEILING: u64 = 200_000; + + pub const MAX_NUMBER_OF_PROPOSALS_WITH_BALLOTS_CEILING: u64 = 700; + + pub const INITIAL_VOTING_PERIOD_SECONDS_CEILING: u64 = 30 * ONE_DAY_SECONDS; + + pub const INITIAL_VOTING_PERIOD_SECONDS_FLOOR: u64 = ONE_DAY_SECONDS; + + pub const WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_CEILING: u64 = 30 * ONE_DAY_SECONDS; + + pub const WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_FLOOR: u64 = 1; + + pub const MAX_FOLLOWEES_PER_FUNCTION_CEILING: u64 = 15; + + pub const MAX_NUMBER_OF_PRINCIPALS_PER_NEURON_CEILING: u64 = 15; + + pub const MAX_DISSOLVE_DELAY_BONUS_PERCENTAGE_CEILING: u64 = 900; + + pub const MAX_AGE_BONUS_PERCENTAGE_CEILING: u64 = 400; + + pub const DEFAULT_MINIMUM_YES_PROPORTION_OF_TOTAL_VOTING_POWER: Percentage = + Percentage::from_basis_points(300); // 3% + + pub const CRITICAL_MINIMUM_YES_PROPORTION_OF_TOTAL_VOTING_POWER: Percentage = + Percentage::from_basis_points(2_000); // 20% + + pub const DEFAULT_MINIMUM_YES_PROPORTION_OF_EXERCISED_VOTING_POWER: Percentage = + Percentage::from_basis_points(5_000); // 50% + + pub const CRITICAL_MINIMUM_YES_PROPORTION_OF_EXERCISED_VOTING_POWER: Percentage = + Percentage::from_basis_points(6_700); // 67% + + pub fn with_default_values() -> Self { + Self { + reject_cost_e8s: Some(E8S_PER_TOKEN), // 1 governance token + neuron_minimum_stake_e8s: Some(E8S_PER_TOKEN), // 1 governance token + transaction_fee_e8s: Some(10_000), + max_proposals_to_keep_per_action: Some(100), + initial_voting_period_seconds: Some(4 * ONE_DAY_SECONDS), // 4d + wait_for_quiet_deadline_increase_seconds: Some(ONE_DAY_SECONDS), // 1d + default_followees: Some(DefaultFollowees::default()), + max_number_of_neurons: Some(200_000), + neuron_minimum_dissolve_delay_to_vote_seconds: Some(6 * ONE_MONTH_SECONDS), // 6m + max_followees_per_function: Some(15), + max_dissolve_delay_seconds: Some(8 * ONE_YEAR_SECONDS), // 8y + max_neuron_age_for_age_bonus: Some(4 * ONE_YEAR_SECONDS), // 4y + max_number_of_proposals_with_ballots: Some(700), + neuron_claimer_permissions: Some(Self::default_neuron_claimer_permissions()), + neuron_grantable_permissions: Some(NeuronPermissionList::default()), + max_number_of_principals_per_neuron: Some(5), + voting_rewards_parameters: Some(VotingRewardsParameters::with_default_values()), + max_dissolve_delay_bonus_percentage: Some(100), + max_age_bonus_percentage: Some(25), + maturity_modulation_disabled: Some(false), + } + } + + fn default_neuron_claimer_permissions() -> NeuronPermissionList { + NeuronPermissionList { + permissions: vec![2, 4, 3], + } + } +} diff --git a/sns-validation/src/validation/sns_init.rs b/sns-validation/src/validation/sns_init.rs new file mode 100644 index 00000000..7272cac9 --- /dev/null +++ b/sns-validation/src/validation/sns_init.rs @@ -0,0 +1,2108 @@ +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + num::NonZeroU64, + str::FromStr, +}; + +use candid::Principal; + +use crate::{ + humanize::E8, + pbs::{ + gov_pb::{NervousSystemParameters, NeuronPermissionList, VotingRewardsParameters}, + sns_pb::{ + sns_init_payload::InitialTokenDistribution, AirdropDistribution, DeveloperDistribution, + FractionalDeveloperVotingPower, NeuronDistribution, SnsInitPayload, SwapDistribution, + }, + }, +}; + +use super::neurons_fund; + +pub const MAX_DAPP_CANISTERS_COUNT: usize = 25; + +pub const MAX_CONFIRMATION_TEXT_LENGTH: usize = 1_000; + +pub const MAX_CONFIRMATION_TEXT_BYTES: usize = 8 * MAX_CONFIRMATION_TEXT_LENGTH; + +pub const MIN_CONFIRMATION_TEXT_LENGTH: usize = 1; + +pub const MAX_FALLBACK_CONTROLLER_PRINCIPAL_IDS_COUNT: usize = 15; + +pub const MAX_DIRECT_ICP_CONTRIBUTION_TO_SWAP: u64 = 1_000_000_000 * E8; + +pub const MAX_NEURONS_FOR_DIRECT_PARTICIPANTS: u64 = 100_000; + +pub const MIN_SNS_NEURONS_PER_BASKET: u64 = 2; + +pub const MAX_SNS_NEURONS_PER_BASKET: u64 = 10; + +enum MinDirectParticipationThresholdValidationError { + // This value must be specified. + Unspecified, + // Needs to be greater or equal the minimum amount of ICP collected from direct participants. + BelowSwapDirectIcpMin { + min_direct_participation_threshold_icp_e8s: u64, + min_direct_participation_icp_e8s: u64, + }, + // Needs to be less than the maximum amount of ICP collected from direct participants. + AboveSwapDirectIcpMax { + min_direct_participation_threshold_icp_e8s: u64, + max_direct_participation_icp_e8s: u64, + }, +} + +impl std::fmt::Display for MinDirectParticipationThresholdValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "MinDirectParticipationThresholdValidationError: "; + match self { + Self::Unspecified => { + write!( + f, + "{}min_direct_participation_threshold_icp_e8s must be specified.", + prefix + ) + } + Self::BelowSwapDirectIcpMin { + min_direct_participation_threshold_icp_e8s, + min_direct_participation_icp_e8s, + } => { + write!( + f, + "{}min_direct_participation_threshold_icp_e8s ({}) should be greater \ + than or equal min_direct_participation_icp_e8s ({}).", + prefix, + min_direct_participation_threshold_icp_e8s, + min_direct_participation_icp_e8s, + ) + } + Self::AboveSwapDirectIcpMax { + min_direct_participation_threshold_icp_e8s, + max_direct_participation_icp_e8s, + } => { + write!( + f, + "{}min_direct_participation_threshold_icp_e8s ({}) should be less \ + than or equal max_direct_participation_icp_e8s ({}).", + prefix, + min_direct_participation_threshold_icp_e8s, + max_direct_participation_icp_e8s, + ) + } + } + } +} + +enum MaxNeuronsFundParticipationValidationError { + // This value must be specified. + Unspecified, + // Does not make sense if no SNS neurons can be created. + BelowSingleParticipationLimit { + max_neurons_fund_participation_icp_e8s: NonZeroU64, + min_participant_icp_e8s: u64, + }, + // The Neuron's Fund should never provide more funds than can be contributed directly. + AboveSwapMaxDirectIcp { + max_neurons_fund_participation_icp_e8s: u64, + max_direct_participation_icp_e8s: u64, + }, +} + +impl std::fmt::Display for MaxNeuronsFundParticipationValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "MaxNeuronsFundParticipationValidationError: "; + match self { + Self::Unspecified => { + write!( + f, + "{}max_neurons_fund_participation_icp_e8s must be specified.", + prefix + ) + } + Self::BelowSingleParticipationLimit { + max_neurons_fund_participation_icp_e8s, + min_participant_icp_e8s, + } => { + write!( + f, + "{}max_neurons_fund_participation_icp_e8s ({} > 0) \ + should be greater than or equal min_participant_icp_e8s ({}).", + prefix, max_neurons_fund_participation_icp_e8s, min_participant_icp_e8s, + ) + } + Self::AboveSwapMaxDirectIcp { + max_neurons_fund_participation_icp_e8s, + max_direct_participation_icp_e8s, + } => { + write!( + f, + "{}max_neurons_fund_participation_icp_e8s ({}) \ + should be less than or equal max_direct_participation_icp_e8s ({}).", + prefix, + max_neurons_fund_participation_icp_e8s, + max_direct_participation_icp_e8s, + ) + } + } + } +} + +enum NeuronsFundParticipationConstraintsValidationError { + SetBeforeProposalExecution, + RelatedFieldUnspecified(String), + MinDirectParticipationThresholdValidationError(MinDirectParticipationThresholdValidationError), + MaxNeuronsFundParticipationValidationError(MaxNeuronsFundParticipationValidationError), + // "Inherit" the remaining, local error cases. + Local(neurons_fund::NeuronsFundParticipationConstraintsValidationError), +} + +impl std::fmt::Display for NeuronsFundParticipationConstraintsValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let prefix = "NeuronsFundParticipationConstraintsValidationError: "; + match self { + Self::SetBeforeProposalExecution => { + write!( + f, + "{}neurons_fund_participation_constraints must not be set before \ + the CreateServiceNervousSystem proposal is executed.", + prefix + ) + } + Self::RelatedFieldUnspecified(related_field_name) => { + write!(f, "{}{} must be specified.", prefix, related_field_name,) + } + Self::MinDirectParticipationThresholdValidationError(error) => { + write!(f, "{}{}", prefix, error) + } + Self::MaxNeuronsFundParticipationValidationError(error) => { + write!(f, "{}{}", prefix, error) + } + Self::Local(error) => write!(f, "{}{}", prefix, error), + } + } +} + +impl From for Result<(), String> { + fn from(value: NeuronsFundParticipationConstraintsValidationError) -> Self { + Err(value.to_string()) + } +} + +#[derive(Clone, Copy)] +pub enum NeuronBasketConstructionParametersValidationError { + ExceedsMaximalDissolveDelay(u64), + ExceedsU64, + BasketSizeTooSmall, + BasketSizeTooBig, + InadequateDissolveDelay, + UnexpectedInLegacyFlow, +} + +impl NeuronBasketConstructionParametersValidationError { + fn field_name() -> String { + "SnsInitPayload.neuron_basket_construction_parameters".to_string() + } +} + +impl std::fmt::Display for NeuronBasketConstructionParametersValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = match self { + Self::ExceedsMaximalDissolveDelay(max_dissolve_delay_seconds) => { + format!( + "must satisfy (count - 1) * dissolve_delay_interval_seconds \ + < SnsInitPayload.max_dissolve_delay_seconds = {max_dissolve_delay_seconds}" + ) + } + Self::BasketSizeTooSmall => format!( + "basket count must be at least {}", + MIN_SNS_NEURONS_PER_BASKET + ), + Self::BasketSizeTooBig => format!( + "basket count must be at most {}", + MAX_SNS_NEURONS_PER_BASKET + ), + Self::InadequateDissolveDelay => { + "dissolve_delay_interval_seconds must be at least 1".to_string() + } + Self::ExceedsU64 => { + format!( + "must satisfy (count - 1) * dissolve_delay_interval_seconds \ + < u64::MAX = {}", + u64::MAX + ) + } + Self::UnexpectedInLegacyFlow => { + "must not be set with the legacy flow for SNS decentralization swaps".to_string() + } + }; + write!(f, "{} {msg}", Self::field_name()) + } +} + +impl From for Result<(), String> { + fn from(val: NeuronBasketConstructionParametersValidationError) -> Self { + Err(val.to_string()) + } +} + +impl FractionalDeveloperVotingPower { + pub(crate) fn swap_distribution(&self) -> Result<&SwapDistribution, String> { + self.swap_distribution + .as_ref() + .ok_or_else(|| "Expected swap distribution to exist".to_string()) + } + + fn validate_neurons( + &self, + developer_distribution: &DeveloperDistribution, + airdrop_distribution: &AirdropDistribution, + nervous_system_parameters: &NervousSystemParameters, + ) -> Result<(), String> { + let neuron_minimum_dissolve_delay_to_vote_seconds = nervous_system_parameters + .neuron_minimum_dissolve_delay_to_vote_seconds + .as_ref() + .expect("Expected NervousSystemParameters.neuron_minimum_dissolve_delay_to_vote_seconds to be set"); + + let max_dissolve_delay_seconds = nervous_system_parameters + .max_dissolve_delay_seconds + .as_ref() + .expect("Expected NervousSystemParameters.max_dissolve_delay_seconds to be set"); + + let missing_developer_principals_count = developer_distribution + .developer_neurons + .iter() + .filter(|neuron_distribution| neuron_distribution.controller.is_none()) + .count(); + + if missing_developer_principals_count != 0 { + return Err(format!( + "Error: {} developer_neurons are missing controllers", + missing_developer_principals_count + )); + } + + let deduped_dev_neurons = developer_distribution + .developer_neurons + .iter() + .map(|neuron_distribution| { + ( + (neuron_distribution.controller, neuron_distribution.memo), + neuron_distribution.stake_e8s, + ) + }) + .collect::>(); + + if deduped_dev_neurons.len() != developer_distribution.developer_neurons.len() { + return Err( + "Error: Neurons with the same controller and memo found in developer_neurons" + .to_string(), + ); + } + + // The max number of DeveloperDistributions that can be specified in the SnsInitPayload. + const MAX_DEVELOPER_DISTRIBUTION_COUNT: usize = 100; + + // The max number of AirdropDistributions that can be specified in the SnsInitPayload. + const MAX_AIRDROP_DISTRIBUTION_COUNT: usize = 1000; + + if deduped_dev_neurons.len() > MAX_DEVELOPER_DISTRIBUTION_COUNT { + return Err(format!( + "Error: The number of developer neurons must be less than {}. Current count is {}", + MAX_DEVELOPER_DISTRIBUTION_COUNT, + deduped_dev_neurons.len(), + )); + } + + // Range of allowed memos for neurons distributed via an SNS swap. This range is used to choose + // the memos of neurons in the neuron basket, and to enforce that other memos (e.g. for Airdrop + // neurons) do not conflict with the neuron basket memos. + const NEURON_BASKET_MEMO_RANGE_START: u64 = 1_000_000; + const SALE_NEURON_MEMO_RANGE_END: u64 = 10_000_000; + + for (controller, memo) in deduped_dev_neurons.keys() { + if NEURON_BASKET_MEMO_RANGE_START <= *memo && *memo <= SALE_NEURON_MEMO_RANGE_END { + return Err(format!( + "Error: Developer neuron with controller {} cannot have a memo in the range {} to {}", + controller.unwrap(), + NEURON_BASKET_MEMO_RANGE_START, + SALE_NEURON_MEMO_RANGE_END + )); + } + } + + let missing_airdrop_principals_count = airdrop_distribution + .airdrop_neurons + .iter() + .filter(|neuron_distribution| neuron_distribution.controller.is_none()) + .count(); + + if missing_airdrop_principals_count != 0 { + return Err(format!( + "Error: {} airdrop_neurons are missing controllers", + missing_airdrop_principals_count + )); + } + + let deduped_airdrop_neurons = airdrop_distribution + .airdrop_neurons + .iter() + .map(|neuron_distribution| { + ( + (neuron_distribution.controller, neuron_distribution.memo), + neuron_distribution.stake_e8s, + ) + }) + .collect::>(); + + if deduped_airdrop_neurons.len() != airdrop_distribution.airdrop_neurons.len() { + return Err( + "Error: Neurons with the same controller and memo detected in airdrop_neurons" + .to_string(), + ); + } + + if deduped_airdrop_neurons.len() > MAX_AIRDROP_DISTRIBUTION_COUNT { + return Err(format!( + "Error: The number of airdrop neurons must be less than {}. Current count is {}", + MAX_AIRDROP_DISTRIBUTION_COUNT, + deduped_airdrop_neurons.len(), + )); + } + + for (controller, memo) in deduped_airdrop_neurons.keys() { + if NEURON_BASKET_MEMO_RANGE_START <= *memo && *memo <= SALE_NEURON_MEMO_RANGE_END { + return Err(format!( + "Error: Airdrop neuron with controller {} cannot have a memo in the range {} to {}", + controller.unwrap(), + NEURON_BASKET_MEMO_RANGE_START, + SALE_NEURON_MEMO_RANGE_END + )); + } + } + + let mut duplicated_neuron_principals = vec![]; + for developer_principal in deduped_dev_neurons.keys() { + if deduped_airdrop_neurons.contains_key(developer_principal) { + // Safe to unwrap due to the checks done above + duplicated_neuron_principals.push(developer_principal.0.unwrap()) + } + } + + if !duplicated_neuron_principals.is_empty() { + return Err(format!( + "Error: The following controllers are present in AirdropDistribution \ + and DeveloperDistribution: {:?}", + duplicated_neuron_principals + )); + } + + let configured_at_least_one_voting_neuron = developer_distribution + .developer_neurons + .iter() + .chain(&airdrop_distribution.airdrop_neurons) + .any(|neuron_distribution| { + neuron_distribution.dissolve_delay_seconds + >= *neuron_minimum_dissolve_delay_to_vote_seconds + }); + + if !configured_at_least_one_voting_neuron { + return Err(format!( + "Error: There needs to be at least one voting-eligible neuron configured. To be \ + eligible to vote, a neuron must have dissolve_delay_seconds of at least {}", + neuron_minimum_dissolve_delay_to_vote_seconds + )); + } + + let misconfigured_dissolve_delay_principals: Vec = developer_distribution + .developer_neurons + .iter() + .chain(&airdrop_distribution.airdrop_neurons) + .filter(|neuron_distribution| { + neuron_distribution.dissolve_delay_seconds > *max_dissolve_delay_seconds + }) + .map(|neuron_distribution| neuron_distribution.controller.unwrap()) + .collect(); + + if !misconfigured_dissolve_delay_principals.is_empty() { + return Err(format!( + "Error: The following PrincipalIds have a dissolve_delay_seconds configured greater than \ + the allowed max_dissolve_delay_seconds ({}): {:?}", max_dissolve_delay_seconds, misconfigured_dissolve_delay_principals + )); + } + + Ok(()) + } + + pub fn validate( + &self, + nervous_system_parameters: &NervousSystemParameters, + ) -> Result<(), String> { + let developer_distribution = self + .developer_distribution + .as_ref() + .ok_or("Error: developer_distribution must be specified")?; + + self.treasury_distribution + .as_ref() + .ok_or("Error: treasury_distribution must be specified")?; + + let swap_distribution = self + .swap_distribution + .as_ref() + .ok_or("Error: swap_distribution must be specified")?; + + let airdrop_distribution = self + .airdrop_distribution + .as_ref() + .ok_or("Error: airdrop_distribution must be specified")?; + + self.validate_neurons( + developer_distribution, + airdrop_distribution, + nervous_system_parameters, + )?; + + match Self::get_total_distributions(&airdrop_distribution.airdrop_neurons) { + Ok(_) => (), + Err(_) => return Err("Error: The sum of all airdrop allocated tokens overflowed and is an invalid distribution".to_string()), + }; + + if swap_distribution.initial_swap_amount_e8s == 0 { + return Err( + "Error: swap_distribution.initial_swap_amount_e8s must be greater than 0" + .to_string(), + ); + } + + if swap_distribution.total_e8s < swap_distribution.initial_swap_amount_e8s { + return Err("Error: swap_distribution.total_e8 must be greater than or equal to swap_distribution.initial_swap_amount_e8s".to_string()); + } + + let total_developer_e8s = match Self::get_total_distributions(&developer_distribution.developer_neurons) { + Ok(total) => total, + Err(_) => return Err("Error: The sum of all developer allocated tokens overflowed and is an invalid distribution".to_string()), + }; + + if total_developer_e8s > swap_distribution.total_e8s { + return Err("Error: The sum of all developer allocated tokens must be less than or equal to swap_distribution.total_e8s".to_string()); + } + + Ok(()) + } + + fn get_total_distributions(distributions: &Vec) -> Result { + let mut distribution_total: u64 = 0; + for distribution in distributions { + distribution_total = match distribution_total.checked_add(distribution.stake_e8s) { + Some(total) => total, + None => { + return Err( + "The total distribution overflowed and is not a valid distribution" + .to_string(), + ) + } + } + } + + Ok(distribution_total) + } +} + +impl SnsInitPayload { + fn get_nervous_system_parameters(&self) -> NervousSystemParameters { + let nervous_system_parameters = NervousSystemParameters::with_default_values(); + let all_permissions = NeuronPermissionList { + permissions: (0..=10).collect(), + }; + + let SnsInitPayload { + transaction_fee_e8s, + token_name: _, + token_symbol: _, + proposal_reject_cost_e8s: reject_cost_e8s, + neuron_minimum_stake_e8s, + fallback_controller_principal_ids: _, + logo: _, + url: _, + name: _, + description: _, + neuron_minimum_dissolve_delay_to_vote_seconds, + reward_rate_transition_duration_seconds, + initial_reward_rate_basis_points, + final_reward_rate_basis_points, + initial_token_distribution: _, + max_dissolve_delay_seconds, + max_neuron_age_seconds_for_age_bonus: max_neuron_age_for_age_bonus, + max_dissolve_delay_bonus_percentage, + max_age_bonus_percentage, + initial_voting_period_seconds, + wait_for_quiet_deadline_increase_seconds, + dapp_canisters: _, + confirmation_text: _, + restricted_countries: _, + min_participants: _, + min_icp_e8s: _, + max_icp_e8s: _, + min_direct_participation_icp_e8s: _, + max_direct_participation_icp_e8s: _, + min_participant_icp_e8s: _, + max_participant_icp_e8s: _, + swap_start_timestamp_seconds: _, + swap_due_timestamp_seconds: _, + neuron_basket_construction_parameters: _, + nns_proposal_id: _, + neurons_fund_participants: _, + token_logo: _, + neurons_fund_participation_constraints: _, + neurons_fund_participation: _, + } = self.clone(); + + let voting_rewards_parameters = Some(VotingRewardsParameters { + reward_rate_transition_duration_seconds, + initial_reward_rate_basis_points, + final_reward_rate_basis_points, + ..nervous_system_parameters.voting_rewards_parameters.unwrap() + }); + + NervousSystemParameters { + neuron_claimer_permissions: Some(all_permissions.clone()), + neuron_grantable_permissions: Some(all_permissions), + transaction_fee_e8s, + reject_cost_e8s, + neuron_minimum_stake_e8s, + neuron_minimum_dissolve_delay_to_vote_seconds, + voting_rewards_parameters, + max_dissolve_delay_seconds, + max_neuron_age_for_age_bonus, + max_dissolve_delay_bonus_percentage, + max_age_bonus_percentage, + initial_voting_period_seconds, + wait_for_quiet_deadline_increase_seconds, + ..nervous_system_parameters + } + } + + fn get_swap_distribution(&self) -> Result<&SwapDistribution, String> { + match &self.initial_token_distribution { + None => Err("Error: initial-token-distribution must be specified".to_string()), + Some(InitialTokenDistribution::FractionalDeveloperVotingPower(f)) => { + f.swap_distribution() + } + } + } + + pub fn validate_pre_execution(&self) -> Result { + let validation_fns = [ + self.validate_token_symbol(), + self.validate_token_name(), + self.validate_token_logo(), + self.validate_token_distribution(), + self.validate_participation_constraints(), + self.validate_neuron_minimum_stake_e8s(), + self.validate_neuron_minimum_dissolve_delay_to_vote_seconds(), + self.validate_neuron_basket_construction_params(), + self.validate_proposal_reject_cost_e8s(), + self.validate_transaction_fee_e8s(), + self.validate_fallback_controller_principal_ids(), + self.validate_url(), + self.validate_logo(), + self.validate_description(), + self.validate_name(), + self.validate_initial_reward_rate_basis_points(), + self.validate_final_reward_rate_basis_points(), + self.validate_reward_rate_transition_duration_seconds(), + self.validate_max_dissolve_delay_seconds(), + self.validate_max_neuron_age_seconds_for_age_bonus(), + self.validate_max_dissolve_delay_bonus_percentage(), + self.validate_max_age_bonus_percentage(), + self.validate_initial_voting_period_seconds(), + self.validate_wait_for_quiet_deadline_increase_seconds(), + self.validate_dapp_canisters(), + self.validate_confirmation_text(), + self.validate_restricted_countries(), + // Ensure that the values that can only be known after the execution + // of the CreateServiceNervousSystem proposal are not set. + self.validate_nns_proposal_id_pre_execution(), + self.validate_neurons_fund_participants_pre_execution(), + self.validate_swap_start_timestamp_seconds_pre_execution(), + self.validate_swap_due_timestamp_seconds_pre_execution(), + self.validate_neurons_fund_participation_constraints(true), + self.validate_neurons_fund_participation(), + // Obsolete fields are not set + self.validate_min_icp_e8s(), + self.validate_max_icp_e8s(), + ]; + + self.join_validation_results(&validation_fns) + } + + pub fn validate_post_execution(&self) -> Result { + let validation_fns = [ + self.validate_token_symbol(), + self.validate_token_name(), + self.validate_token_logo(), + self.validate_token_distribution(), + self.validate_neuron_minimum_stake_e8s(), + self.validate_neuron_minimum_dissolve_delay_to_vote_seconds(), + self.validate_proposal_reject_cost_e8s(), + self.validate_transaction_fee_e8s(), + self.validate_fallback_controller_principal_ids(), + self.validate_url(), + self.validate_logo(), + self.validate_description(), + self.validate_name(), + self.validate_initial_reward_rate_basis_points(), + self.validate_final_reward_rate_basis_points(), + self.validate_reward_rate_transition_duration_seconds(), + self.validate_max_dissolve_delay_seconds(), + self.validate_max_neuron_age_seconds_for_age_bonus(), + self.validate_max_dissolve_delay_bonus_percentage(), + self.validate_max_age_bonus_percentage(), + self.validate_initial_voting_period_seconds(), + self.validate_wait_for_quiet_deadline_increase_seconds(), + self.validate_dapp_canisters(), + self.validate_confirmation_text(), + self.validate_restricted_countries(), + self.validate_all_post_execution_swap_parameters_are_set(), + self.validate_neuron_basket_construction_params(), + self.validate_min_participants(), + self.validate_min_icp_e8s(), + self.validate_max_icp_e8s(), + self.validate_min_direct_participation_icp_e8s(), + self.validate_max_direct_participation_icp_e8s(), + self.validate_min_participant_icp_e8s(), + self.validate_max_participant_icp_e8s(), + self.validate_nns_proposal_id(), + self.validate_neurons_fund_participants(), + self.validate_swap_start_timestamp_seconds(), + self.validate_swap_due_timestamp_seconds(), + self.validate_neurons_fund_participation_constraints(false), + self.validate_neurons_fund_participation(), + ]; + + self.join_validation_results(&validation_fns) + } + + fn join_validation_results( + &self, + validation_fns: &[Result<(), String>], + ) -> Result { + let mut seen_messages = HashSet::new(); + let defect_messages = validation_fns + .iter() + .filter_map(|validation_fn| match validation_fn { + Err(msg) => Some(msg), + Ok(_) => None, + }) + .filter(|&x| + // returns true iff the set did not already contain the value + seen_messages.insert(x.clone())) + .cloned() + .collect::>() + .join("\n"); + + if defect_messages.is_empty() { + Ok(self.clone()) + } else { + Err(defect_messages) + } + } + + fn validate_token_symbol(&self) -> Result<(), String> { + let token_symbol = self + .token_symbol + .as_ref() + .ok_or_else(|| "Error: token-symbol must be specified".to_string())?; + + // The maximum number of characters allowed for token symbol. + const MAX_TOKEN_SYMBOL_LENGTH: usize = 10; + + // The minimum number of characters allowed for token symbol. + const MIN_TOKEN_SYMBOL_LENGTH: usize = 3; + + // Token Symbols that can not be used. + const BANNED_TOKEN_SYMBOLS: &[&str] = &["ICP", "DFINITY"]; + + if token_symbol.len() > MAX_TOKEN_SYMBOL_LENGTH { + return Err(format!( + "Error: token-symbol must be fewer than {} characters, given character count: {}", + MAX_TOKEN_SYMBOL_LENGTH, + token_symbol.len() + )); + } + + if token_symbol.len() < MIN_TOKEN_SYMBOL_LENGTH { + return Err(format!( + "Error: token-symbol must be greater than {} characters, given character count: {}", + MIN_TOKEN_SYMBOL_LENGTH, + token_symbol.len() + )); + } + + if token_symbol != token_symbol.trim() { + return Err("Token symbol must not have leading or trailing whitespaces".to_string()); + } + + if BANNED_TOKEN_SYMBOLS.contains(&token_symbol.to_uppercase().as_ref()) { + return Err("Banned token symbol, please chose another one.".to_string()); + } + + Ok(()) + } + + fn validate_token_name(&self) -> Result<(), String> { + let token_name = self + .token_name + .as_ref() + .ok_or_else(|| "Error: token-name must be specified".to_string())?; + + // The maximum number of characters allowed for token name. + const MAX_TOKEN_NAME_LENGTH: usize = 255; + + // The minimum number of characters allowed for token name. + const MIN_TOKEN_NAME_LENGTH: usize = 4; + + // Token Names that can not be used. + const BANNED_TOKEN_NAMES: &[&str] = &["internetcomputer", "internetcomputerprotocol"]; + + if token_name.len() > MAX_TOKEN_NAME_LENGTH { + return Err(format!( + "Error: token-name must be fewer than {} characters, given character count: {}", + MAX_TOKEN_NAME_LENGTH, + token_name.len() + )); + } + + if token_name.len() < MIN_TOKEN_NAME_LENGTH { + return Err(format!( + "Error: token-name must be greater than {} characters, given character count: {}", + MIN_TOKEN_NAME_LENGTH, + token_name.len() + )); + } + + if token_name != token_name.trim() { + return Err("Token name must not have leading or trailing whitespaces".to_string()); + } + + if BANNED_TOKEN_NAMES.contains( + &token_name + .to_lowercase() + .chars() + .filter(|c| !c.is_whitespace()) + .collect::() + .as_ref(), + ) { + return Err("Banned token name, please chose another one.".to_string()); + } + + Ok(()) + } + + fn validate_token_logo(&self) -> Result<(), String> { + let token_logo = self + .token_logo + .as_ref() + .ok_or_else(|| "Error: token_logo must be specified".to_string())?; + + const PREFIX: &str = "data:image/png;base64,"; + // The maximum number of characters allowed for a SNS logo encoding. + // Roughly 256Kb + const MAX_LOGO_LENGTH: usize = 341334; + + if token_logo.len() > MAX_LOGO_LENGTH { + return Err(format!( + "Error: token_logo must be less than {} characters, roughly 256 Kb", + MAX_LOGO_LENGTH + )); + } + + if !token_logo.starts_with(PREFIX) { + return Err(format!( + "Error: token_logo must be a base64 encoded PNG, but the provided \ + string doesn't begin with `{PREFIX}`." + )); + } + + // TODO: add b64 validation + // if base64::decode(&token_logo[PREFIX.len()..]).is_err() { + // return Err("Couldn't decode base64 in SnsMetadata.logo".to_string()); + // } + + Ok(()) + } + + fn validate_token_distribution(&self) -> Result<(), String> { + let initial_token_distribution = self + .initial_token_distribution + .as_ref() + .ok_or_else(|| "Error: initial-token-distribution must be specified".to_string())?; + + let nervous_system_parameters = self.get_nervous_system_parameters(); + + match initial_token_distribution { + InitialTokenDistribution::FractionalDeveloperVotingPower(f) => { + f.validate(&nervous_system_parameters)? + } + } + + Ok(()) + } + + fn validate_transaction_fee_e8s(&self) -> Result<(), String> { + match self.transaction_fee_e8s { + Some(_) => Ok(()), + None => Err("Error: transaction_fee_e8s must be specified.".to_string()), + } + } + + fn validate_proposal_reject_cost_e8s(&self) -> Result<(), String> { + match self.proposal_reject_cost_e8s { + Some(_) => Ok(()), + None => Err("Error: proposal_reject_cost_e8s must be specified.".to_string()), + } + } + + fn validate_neuron_minimum_stake_e8s(&self) -> Result<(), String> { + let neuron_minimum_stake_e8s = self + .neuron_minimum_stake_e8s + .expect("Error: neuron_minimum_stake_e8s must be specified."); + let initial_token_distribution = self + .initial_token_distribution + .as_ref() + .ok_or_else(|| "Error: initial-token-distribution must be specified".to_string())?; + + match initial_token_distribution { + InitialTokenDistribution::FractionalDeveloperVotingPower(f) => { + let developer_distribution = f + .developer_distribution + .as_ref() + .ok_or_else(|| "Error: developer_distribution must be specified".to_string())?; + + let airdrop_distribution = f + .airdrop_distribution + .as_ref() + .ok_or_else(|| "Error: airdrop_distribution must be specified".to_string())?; + + let min_stake_infringing_developer_neurons: Vec<(Principal, u64)> = + developer_distribution + .developer_neurons + .iter() + .filter_map(|neuron_distribution| { + if neuron_distribution.stake_e8s < neuron_minimum_stake_e8s { + // Safe to unwrap due to the checks done above + Some(( + neuron_distribution.controller.unwrap(), + neuron_distribution.stake_e8s, + )) + } else { + None + } + }) + .collect(); + + if !min_stake_infringing_developer_neurons.is_empty() { + return Err(format!( + "Error: {} developer neurons have a stake below the minimum stake ({} e8s): \n {:?}", + min_stake_infringing_developer_neurons.len(), + neuron_minimum_stake_e8s, + min_stake_infringing_developer_neurons, + )); + } + + let min_stake_infringing_airdrop_neurons: Vec<(Principal, u64)> = + airdrop_distribution + .airdrop_neurons + .iter() + .filter_map(|neuron_distribution| { + if neuron_distribution.stake_e8s < neuron_minimum_stake_e8s { + // Safe to unwrap due to the checks done above + Some(( + neuron_distribution.controller.unwrap(), + neuron_distribution.stake_e8s, + )) + } else { + None + } + }) + .collect(); + + if !min_stake_infringing_airdrop_neurons.is_empty() { + return Err(format!( + "Error: {} airdrop neurons have a stake below the minimum stake ({} e8s): \n {:?}", + min_stake_infringing_airdrop_neurons.len(), + neuron_minimum_stake_e8s, + min_stake_infringing_airdrop_neurons, + )); + } + } + } + + Ok(()) + } + + fn validate_neuron_minimum_dissolve_delay_to_vote_seconds(&self) -> Result<(), String> { + // As this is not currently configurable, pull the default value from + let max_dissolve_delay_seconds = *NervousSystemParameters::with_default_values() + .max_dissolve_delay_seconds + .as_ref() + .unwrap(); + + let neuron_minimum_dissolve_delay_to_vote_seconds = self + .neuron_minimum_dissolve_delay_to_vote_seconds + .ok_or_else(|| { + "Error: neuron-minimum-dissolve-delay-to-vote-seconds must be specified".to_string() + })?; + + if neuron_minimum_dissolve_delay_to_vote_seconds > max_dissolve_delay_seconds { + return Err(format!( + "The minimum dissolve delay to vote ({}) cannot be greater than the max \ + dissolve delay ({})", + neuron_minimum_dissolve_delay_to_vote_seconds, max_dissolve_delay_seconds + )); + } + + Ok(()) + } + + fn validate_fallback_controller_principal_ids(&self) -> Result<(), String> { + if self.fallback_controller_principal_ids.is_empty() { + return Err( + "Error: At least one principal ID must be supplied as a fallback controller \ + in case the initial token swap fails." + .to_string(), + ); + } + + if self.fallback_controller_principal_ids.len() + > MAX_FALLBACK_CONTROLLER_PRINCIPAL_IDS_COUNT + { + return Err(format!( + "Error: The number of fallback_controller_principal_ids \ + must be less than {}. Current count is {}", + MAX_FALLBACK_CONTROLLER_PRINCIPAL_IDS_COUNT, + self.fallback_controller_principal_ids.len() + )); + } + + let (valid_principals, invalid_principals): (Vec<_>, Vec<_>) = self + .fallback_controller_principal_ids + .iter() + .map(|principal_id_string| { + ( + principal_id_string, + Principal::from_str(principal_id_string), + ) + }) + .partition(|item| item.1.is_ok()); + + if !invalid_principals.is_empty() { + return Err(format!( + "Error: One or more fallback_controller_principal_ids is not a valid principal id. \ + The follow principals are invalid: {:?}", + invalid_principals + .into_iter() + .map(|pair| pair.0) + .collect::>() + )); + } + + // At this point, all principals are valid. Dedupe the values + let unique_principals: BTreeSet<_> = valid_principals + .iter() + .filter_map(|pair| pair.1.clone().ok()) + .collect(); + + if unique_principals.len() != valid_principals.len() { + return Err( + "Error: Duplicate PrincipalIds found in fallback_controller_principal_ids" + .to_string(), + ); + } + + Ok(()) + } + + fn validate_logo(&self) -> Result<(), String> { + let logo = self + .logo + .as_ref() + .ok_or_else(|| "Error: logo must be specified".to_string())?; + + const PREFIX: &str = "data:image/png;base64,"; + const MAX_LOGO_LENGTH: usize = 341334; + + // TODO: Should we check that it's a valid PNG? + if logo.len() > MAX_LOGO_LENGTH { + return Err(format!( + "SnsMetadata.logo must be less than {} characters, roughly 256 Kb", + MAX_LOGO_LENGTH + )); + } + if !logo.starts_with(PREFIX) { + return Err(format!("SnsMetadata.logo must be a base64 encoded PNG, but the provided string does't begin with `{PREFIX}`.")); + } + + // TODO: add b64 validation + // if base64::decode(&logo[PREFIX.len()..]).is_err() { + // return Err("Couldn't decode base64 in SnsMetadata.logo".to_string()); + // } + Ok(()) + } + + fn validate_url(&self) -> Result<(), String> { + let url = self.url.as_ref().ok_or("Error: url must be specified")?; + let field_name = "SnsMetadata.url"; + let max_length = 512; + let min_length = 10; + // // Check that the URL is a sensible length + if url.len() > max_length { + return Err(format!( + "{field_name} must be less than {max_length} characters long, but it is {} characters long. (Field was set to `{url}`.)", + url.len(), + )); + } + if url.len() < min_length { + return Err(format!( + "{field_name} must be greater or equal to than {min_length} characters long, but it is {} characters long. (Field was set to `{url}`.)", + url.len(), + )); + } + + // + + if !url.starts_with("https://") { + return Err(format!( + "{field_name} must begin with https://. (Field was set to `{url}`.)", + )); + } + + let parts_url: Vec<&str> = url.split("://").collect(); + if parts_url.len() > 2 { + return Err(format!( + "{field_name} contains an invalid sequence of characters" + )); + } + + if parts_url.len() < 2 { + return Err(format!("{field_name} is missing content after protocol.")); + } + + if url.contains('@') { + return Err(format!( + "{field_name} cannot contain authentication information" + )); + } + + let parts_past_protocol = parts_url[1].split_once('/'); + + let (_domain, _path) = match parts_past_protocol { + Some((domain, path)) => (domain, Some(path)), + None => (parts_url[1], None), + }; + Ok(()) + } + + fn validate_name(&self) -> Result<(), String> { + // The maximum number of characters allowed for a SNS name. + const MAX_NAME_LENGTH: usize = 255; + + // The minimum number of characters allowed for a SNS name. + const MIN_NAME_LENGTH: usize = 4; + let name = self.name.as_ref().ok_or("Error: name must be specified")?; + if name.len() > MAX_NAME_LENGTH { + return Err(format!( + "SnsMetadata.name must be less than {} characters", + MAX_NAME_LENGTH + )); + } else if name.len() < MIN_NAME_LENGTH { + return Err(format!( + "SnsMetadata.name must be greater than {} characters", + MIN_NAME_LENGTH + )); + } + Ok(()) + } + + fn validate_description(&self) -> Result<(), String> { + // The maximum number of characters allowed for a SNS description. + const MAX_DESCRIPTION_LENGTH: usize = 2000; + + // The minimum number of characters allowed for a SNS description. + const MIN_DESCRIPTION_LENGTH: usize = 10; + let description = self + .description + .as_ref() + .ok_or("Error: description must be specified")?; + + if description.len() > MAX_DESCRIPTION_LENGTH { + return Err(format!( + "SnsMetadata.description must be less than {} characters", + MAX_DESCRIPTION_LENGTH + )); + } else if description.len() < MIN_DESCRIPTION_LENGTH { + return Err(format!( + "SnsMetadata.description must be greater than {} characters", + MIN_DESCRIPTION_LENGTH + )); + } + Ok(()) + } + + fn validate_initial_reward_rate_basis_points(&self) -> Result<(), String> { + let initial_reward_rate_basis_points = self + .initial_reward_rate_basis_points + .ok_or("Error: initial_reward_rate_basis_points must be specified")?; + if initial_reward_rate_basis_points + > VotingRewardsParameters::INITIAL_REWARD_RATE_BASIS_POINTS_CEILING + { + Err(format!( + "Error: initial_reward_rate_basis_points must be less than or equal to {}", + VotingRewardsParameters::INITIAL_REWARD_RATE_BASIS_POINTS_CEILING + )) + } else { + Ok(()) + } + } + + fn validate_final_reward_rate_basis_points(&self) -> Result<(), String> { + let initial_reward_rate_basis_points = self + .initial_reward_rate_basis_points + .ok_or("Error: initial_reward_rate_basis_points must be specified")?; + let final_reward_rate_basis_points = self + .final_reward_rate_basis_points + .ok_or("Error: final_reward_rate_basis_points must be specified")?; + if final_reward_rate_basis_points > initial_reward_rate_basis_points { + Err( + format!( + "Error: final_reward_rate_basis_points ({}) must be less than or equal to initial_reward_rate_basis_points ({})", final_reward_rate_basis_points, + initial_reward_rate_basis_points + ) + ) + } else { + Ok(()) + } + } + + fn validate_reward_rate_transition_duration_seconds(&self) -> Result<(), String> { + let _reward_rate_transition_duration_seconds = self + .reward_rate_transition_duration_seconds + .ok_or("Error: reward_rate_transition_duration_seconds must be specified")?; + Ok(()) + } + + fn validate_max_dissolve_delay_seconds(&self) -> Result<(), String> { + let _max_dissolve_delay_seconds = self + .max_dissolve_delay_seconds + .ok_or("Error: max_dissolve_delay_seconds must be specified")?; + Ok(()) + } + + fn validate_max_neuron_age_seconds_for_age_bonus(&self) -> Result<(), String> { + let _max_neuron_age_seconds_for_age_bonus = self + .max_neuron_age_seconds_for_age_bonus + .ok_or("Error: max_neuron_age_seconds_for_age_bonus must be specified")?; + Ok(()) + } + + fn validate_max_dissolve_delay_bonus_percentage(&self) -> Result<(), String> { + let max_dissolve_delay_bonus_percentage = self + .max_dissolve_delay_bonus_percentage + .ok_or("Error: max_dissolve_delay_bonus_percentage must be specified")?; + + if max_dissolve_delay_bonus_percentage + > NervousSystemParameters::MAX_DISSOLVE_DELAY_BONUS_PERCENTAGE_CEILING + { + Err(format!( + "max_dissolve_delay_bonus_percentage must be less than {}", + NervousSystemParameters::MAX_DISSOLVE_DELAY_BONUS_PERCENTAGE_CEILING + )) + } else { + Ok(()) + } + } + + fn validate_max_age_bonus_percentage(&self) -> Result<(), String> { + let max_age_bonus_percentage = self + .max_age_bonus_percentage + .ok_or("Error: max_age_bonus_percentage must be specified")?; + if max_age_bonus_percentage > NervousSystemParameters::MAX_AGE_BONUS_PERCENTAGE_CEILING { + Err(format!( + "max_age_bonus_percentage must be less than {}", + NervousSystemParameters::MAX_AGE_BONUS_PERCENTAGE_CEILING + )) + } else { + Ok(()) + } + } + + fn validate_initial_voting_period_seconds(&self) -> Result<(), String> { + let initial_voting_period_seconds = self + .initial_voting_period_seconds + .ok_or("Error: initial_voting_period_seconds must be specified")?; + + if initial_voting_period_seconds + < NervousSystemParameters::INITIAL_VOTING_PERIOD_SECONDS_FLOOR + { + Err(format!( + "NervousSystemParameters.initial_voting_period_seconds must be greater than {}", + NervousSystemParameters::INITIAL_VOTING_PERIOD_SECONDS_FLOOR + )) + } else if initial_voting_period_seconds + > NervousSystemParameters::INITIAL_VOTING_PERIOD_SECONDS_CEILING + { + Err(format!( + "NervousSystemParameters.initial_voting_period_seconds must be less than {}", + NervousSystemParameters::INITIAL_VOTING_PERIOD_SECONDS_CEILING + )) + } else { + Ok(()) + } + } + + fn validate_wait_for_quiet_deadline_increase_seconds(&self) -> Result<(), String> { + let wait_for_quiet_deadline_increase_seconds = self + .wait_for_quiet_deadline_increase_seconds + .ok_or("Error: wait_for_quiet_deadline_increase_seconds must be specified")?; + let initial_voting_period_seconds = self + .initial_voting_period_seconds + .ok_or("Error: initial_voting_period_seconds must be specified")?; + + if wait_for_quiet_deadline_increase_seconds + < NervousSystemParameters::WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_FLOOR + { + Err(format!( + "NervousSystemParameters.wait_for_quiet_deadline_increase_seconds must be greater than or equal to {}", + NervousSystemParameters::WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_FLOOR + )) + } else if wait_for_quiet_deadline_increase_seconds + > NervousSystemParameters::WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_CEILING + { + Err(format!( + "NervousSystemParameters.wait_for_quiet_deadline_increase_seconds must be less than or equal to {}", + NervousSystemParameters::WAIT_FOR_QUIET_DEADLINE_INCREASE_SECONDS_CEILING + )) + // If `wait_for_quiet_deadline_increase_seconds > initial_voting_period_seconds / 2`, any flip (including an initial `yes` vote) + // will always cause the deadline to be increased. That seems like unreasonable behavior, so we prevent that from being + // the case. + } else if wait_for_quiet_deadline_increase_seconds > initial_voting_period_seconds / 2 { + Err(format!( + "NervousSystemParameters.wait_for_quiet_deadline_increase_seconds is {}, but must be less than or equal to half the initial voting period, {}", + initial_voting_period_seconds, initial_voting_period_seconds / 2 + )) + } else { + Ok(()) + } + } + + fn validate_dapp_canisters(&self) -> Result<(), String> { + let dapp_canisters = match &self.dapp_canisters { + None => return Ok(()), + Some(dapp_canisters) => dapp_canisters, + }; + + if dapp_canisters.canisters.len() > MAX_DAPP_CANISTERS_COUNT { + return Err(format!( + "Error: The number of dapp_canisters exceeded the maximum allowed canisters at \ + initialization. Count is {}. Maximum allowed is {}.", + dapp_canisters.canisters.len(), + MAX_DAPP_CANISTERS_COUNT, + )); + } + + for (index, canister) in dapp_canisters.canisters.iter().enumerate() { + if canister.id.is_none() { + return Err(format!("Error: dapp_canisters[{}] id field is None", index)); + } + } + + // Disallow duplicate dapp canisters, because it indicates that + // the user probably made a mistake (e.g. copy n' paste). + let unique_dapp_canisters: BTreeSet<_> = dapp_canisters + .canisters + .iter() + .map(|canister| canister.id) + .collect(); + if unique_dapp_canisters.len() != dapp_canisters.canisters.len() { + return Err("Error: Duplicate ids found in dapp_canisters".to_string()); + } + + // let nns_canisters = &[ + // NNS_GOVERNANCE_CANISTER_ID, + // ICP_LEDGER_CANISTER_ID, + // REGISTRY_CANISTER_ID, + // ROOT_CANISTER_ID, + // CYCLES_MINTING_CANISTER_ID, + // LIFELINE_CANISTER_ID, + // GENESIS_TOKEN_CANISTER_ID, + // IDENTITY_CANISTER_ID, + // NNS_UI_CANISTER_ID, + // SNS_WASM_CANISTER_ID, + // EXCHANGE_RATE_CANISTER_ID, + // ] + // .map(Principal::from); + + // let nns_canisters_listed_as_dapp = dapp_canisters + // .canisters + // .iter() + // .filter_map(|canister| { + // // Will not fail because of previous check + // let id = canister.id.unwrap(); + // if nns_canisters.contains(&id) { + // Some(id) + // } else { + // None + // } + // }) + // .collect::>(); + // if !nns_canisters_listed_as_dapp.is_empty() { + // return Err(format!( + // "Error: The following canisters are listed as dapp canisters, but are \ + // NNS canisters: {:?}", + // nns_canisters_listed_as_dapp + // )); + // } + + Ok(()) + } + + fn validate_confirmation_text(&self) -> Result<(), String> { + if let Some(confirmation_text) = &self.confirmation_text { + if MAX_CONFIRMATION_TEXT_BYTES < confirmation_text.len() { + return Err( + format!( + "NervousSystemParameters.confirmation_text must be fewer than {} bytes, given bytes: {}", + MAX_CONFIRMATION_TEXT_BYTES, + confirmation_text.len(), + ) + ); + } + let confirmation_text_length = confirmation_text.chars().count(); + if confirmation_text_length < MIN_CONFIRMATION_TEXT_LENGTH { + return Err( + format!( + "NervousSystemParameters.confirmation_text must be greater than {} characters, given character count: {}", + MIN_CONFIRMATION_TEXT_LENGTH, + confirmation_text_length, + ) + ); + } + if MAX_CONFIRMATION_TEXT_LENGTH < confirmation_text_length { + return Err( + format!( + "NervousSystemParameters.confirmation_text must be fewer than {} characters, given character count: {}", + MAX_CONFIRMATION_TEXT_LENGTH, + confirmation_text_length, + ) + ); + } + } + Ok(()) + } + + fn validate_restricted_countries(&self) -> Result<(), String> { + // if let Some(restricted_countries) = &self.restricted_countries { + // if restricted_countries.iso_codes.is_empty() { + // return RestrictedCountriesValidationError::EmptyList.into(); + // } + // let num_items = restricted_countries.iso_codes.len(); + // if CountryCode::num_country_codes() < num_items { + // return RestrictedCountriesValidationError::TooManyItems( + // restricted_countries.iso_codes.len(), + // ) + // .into(); + // } + // let mut unique_iso_codes = BTreeSet::::new(); + // for item in &restricted_countries.iso_codes { + // if CountryCode::for_alpha2(item).is_err() { + // return RestrictedCountriesValidationError::NotIsoCompliant(item.clone()) + // .into(); + // } + // if !unique_iso_codes.insert(item.clone()) { + // return RestrictedCountriesValidationError::ContainsDuplicates(item.clone()) + // .into(); + // } + // } + // } + Ok(()) + } + + fn validate_neuron_basket_construction_params(&self) -> Result<(), String> { + let neuron_basket_construction_parameters = self + .neuron_basket_construction_parameters + .as_ref() + .ok_or("Error: neuron_basket_construction_parameters must be specified")?; + + // Check that `NeuronBasket` dissolve delay does not exceed + // the maximum dissolve delay. + let max_dissolve_delay_seconds = self + .max_dissolve_delay_seconds + .ok_or("Error: max_dissolve_delay_seconds must be specified")?; + // The maximal dissolve delay of a neuron from a basket created by + // `NeuronBasketConstructionParameters::generate_vesting_schedule` + // will equal `(count - 1) * dissolve_delay_interval_seconds`. + let max_neuron_basket_dissolve_delay = neuron_basket_construction_parameters + .count + .saturating_sub(1_u64) + .checked_mul(neuron_basket_construction_parameters.dissolve_delay_interval_seconds); + if let Some(max_neuron_basket_dissolve_delay) = max_neuron_basket_dissolve_delay { + if max_neuron_basket_dissolve_delay > max_dissolve_delay_seconds { + return NeuronBasketConstructionParametersValidationError::ExceedsMaximalDissolveDelay(max_dissolve_delay_seconds) + .into(); + } + } else { + return NeuronBasketConstructionParametersValidationError::ExceedsU64.into(); + } + if neuron_basket_construction_parameters.count < MIN_SNS_NEURONS_PER_BASKET { + return NeuronBasketConstructionParametersValidationError::BasketSizeTooSmall.into(); + } + if neuron_basket_construction_parameters.count > MAX_SNS_NEURONS_PER_BASKET { + return NeuronBasketConstructionParametersValidationError::BasketSizeTooBig.into(); + } + if neuron_basket_construction_parameters.dissolve_delay_interval_seconds < 1 { + return NeuronBasketConstructionParametersValidationError::InadequateDissolveDelay + .into(); + } + Ok(()) + } + + fn validate_min_participants(&self) -> Result<(), String> { + let min_participants = self + .min_participants + .ok_or("Error: min_participants must be specified")?; + + if min_participants == 0 { + return Err("Error: min_participants must be > 0".to_string()); + } + + // Needed as the SwapInit min_participants field is a u32 + if min_participants > (u32::MAX as u64) { + return Err(format!( + "Error: min_participants cannot be greater than {}", + u32::MAX + )); + } + + Ok(()) + } + + fn validate_min_direct_participation_icp_e8s(&self) -> Result<(), String> { + let min_direct_participation_icp_e8s = self + .min_direct_participation_icp_e8s + .ok_or("Error: min_direct_participation_icp_e8s must be specified")?; + + if min_direct_participation_icp_e8s == 0 { + return Err("Error: min_direct_participation_icp_e8s must be > 0".to_string()); + } + + Ok(()) + } + + fn validate_max_icp_e8s(&self) -> Result<(), String> { + if self.max_icp_e8s.is_some() { + return Err( + "Error: max_icp_e8s cannot be specified now that Matched Funding is enabled" + .to_string(), + ); + } + + Ok(()) + } + + fn validate_min_icp_e8s(&self) -> Result<(), String> { + if self.min_icp_e8s.is_some() { + return Err( + "Error: min_icp_e8s cannot be specified now that Matched Funding is enabled" + .to_string(), + ); + }; + + Ok(()) + } + + fn validate_max_direct_participation_icp_e8s(&self) -> Result<(), String> { + let max_direct_participation_icp_e8s = self + .max_direct_participation_icp_e8s + .ok_or("Error: max_direct_participation_icp_e8s must be specified")?; + + let min_direct_participation_icp_e8s = self + .min_direct_participation_icp_e8s + .ok_or("Error: min_direct_participation_icp_e8s must be specified")?; + + if max_direct_participation_icp_e8s < min_direct_participation_icp_e8s { + return Err(format!( + "max_direct_participation_icp_e8s ({}) must be >= min_direct_participation_icp_e8s ({})", + max_direct_participation_icp_e8s, min_direct_participation_icp_e8s + )); + } + + if max_direct_participation_icp_e8s > MAX_DIRECT_ICP_CONTRIBUTION_TO_SWAP { + return Err(format!( + "Error: max_direct_participation_icp_e8s ({}) can be at most {} ICP E8s", + max_direct_participation_icp_e8s, MAX_DIRECT_ICP_CONTRIBUTION_TO_SWAP + )); + } + + let min_participants = self + .min_participants + .ok_or("Error: min_participants must be specified")?; + + let min_participant_icp_e8s = self + .min_participant_icp_e8s + .ok_or("Error: min_participant_icp_e8s must be specified")?; + + if max_direct_participation_icp_e8s + < min_participants.saturating_mul(min_participant_icp_e8s) + { + return Err(format!( + "Error: max_direct_participation_icp_e8s ({}) must be >= min_participants ({}) * min_participant_icp_e8s ({})", + max_direct_participation_icp_e8s, min_participants, min_participant_icp_e8s + )); + } + + Ok(()) + } + + fn validate_min_participant_icp_e8s(&self) -> Result<(), String> { + let min_participant_icp_e8s = self + .min_participant_icp_e8s + .ok_or("Error: min_participant_icp_e8s must be specified")?; + + let max_direct_participation_icp_e8s = self + .max_direct_participation_icp_e8s + .ok_or("Error: max_direct_participation_icp_e8s must be specified")?; + + let sns_transaction_fee_e8s = self + .transaction_fee_e8s + .ok_or("Error: transaction_fee_e8s must be specified")?; + + let neuron_minimum_stake_e8s = self + .neuron_minimum_stake_e8s + .ok_or("Error: neuron_minimum_stake_e8s must be specified")?; + + let neuron_basket_construction_parameters_count = self + .neuron_basket_construction_parameters + .as_ref() + .ok_or("Error: neuron_basket_construction_parameters must be specified")? + .count; + + let sns_tokens_e8s = self + .get_swap_distribution() + .map_err(|_| "Error: the SwapDistribution must be specified")? + .initial_swap_amount_e8s; + + let min_participant_sns_e8s = min_participant_icp_e8s as u128 * sns_tokens_e8s as u128 + / max_direct_participation_icp_e8s as u128; + + if neuron_minimum_stake_e8s <= sns_transaction_fee_e8s { + return Err(format!( + "Error: neuron_minimum_stake_e8s={} is too small. It needs to be \ + greater than the transaction fee ({} e8s)", + neuron_minimum_stake_e8s, sns_transaction_fee_e8s + )); + } + + let min_participant_icp_e8s_big_enough = min_participant_sns_e8s + >= neuron_basket_construction_parameters_count as u128 + * (neuron_minimum_stake_e8s + sns_transaction_fee_e8s) as u128; + + if !min_participant_icp_e8s_big_enough { + return Err(format!( + "Error: min_participant_icp_e8s={} is too small. It needs to be \ + large enough to ensure that participants will end up with \ + enough SNS tokens to form {} SNS neurons, each of which \ + require at least {} SNS e8s, plus {} e8s in transaction \ + fees. More precisely, the following inequality must hold: \ + min_participant_icp_e8s >= neuron_basket_count * \ + (neuron_minimum_stake_e8s + transaction_fee_e8s) * max_icp_e8s / sns_tokens_e8s", + min_participant_icp_e8s, + neuron_basket_construction_parameters_count, + neuron_minimum_stake_e8s, + sns_transaction_fee_e8s, + )); + } + + Ok(()) + } + + fn validate_max_participant_icp_e8s(&self) -> Result<(), String> { + let max_participant_icp_e8s = self + .max_participant_icp_e8s + .ok_or("Error: max_participant_icp_e8s must be specified")?; + + let min_participant_icp_e8s = self + .min_participant_icp_e8s + .ok_or("Error: min_participant_icp_e8s must be specified")?; + + if max_participant_icp_e8s < min_participant_icp_e8s { + return Err(format!( + "Error: max_participant_icp_e8s ({}) must be >= min_participant_icp_e8s ({})", + max_participant_icp_e8s, min_participant_icp_e8s + )); + } + + let max_direct_participation_icp_e8s = self + .max_direct_participation_icp_e8s + .ok_or("Error: max_direct_participation_icp_e8s must be specified")?; + + if max_participant_icp_e8s > max_direct_participation_icp_e8s { + return Err(format!( + "max_participant_icp_e8s ({}) must be <= max_direct_participation_icp_e8s ({})", + max_participant_icp_e8s, max_direct_participation_icp_e8s + )); + } + + Ok(()) + } + + fn validate_participation_constraints(&self) -> Result<(), String> { + // (1) + let min_direct_participation_icp_e8s = self + .min_direct_participation_icp_e8s + .ok_or("Error: min_direct_participation_icp_e8s must be specified")?; + + let max_direct_participation_icp_e8s = self + .max_direct_participation_icp_e8s + .ok_or("Error: max_direct_participation_icp_e8s must be specified")?; + + let min_participant_icp_e8s = self + .min_participant_icp_e8s + .ok_or("Error: min_participant_icp_e8s must be specified")?; + + let max_participant_icp_e8s = self + .max_participant_icp_e8s + .ok_or("Error: max_participant_icp_e8s must be specified")?; + + let min_participants = self + .min_participants + .ok_or("Error: min_participants must be specified")?; + + let initial_swap_amount_e8s = self + .get_swap_distribution() + .map_err(|_| "Error: the SwapDistribution must be specified")? + .initial_swap_amount_e8s; + + let neuron_basket_construction_parameters_count = self + .neuron_basket_construction_parameters + .as_ref() + .ok_or("Error: neuron_basket_construction_parameters must be specified")? + .count; + + let neuron_minimum_stake_e8s = self + .neuron_minimum_stake_e8s + .ok_or("Error: neuron_minimum_stake_e8s must be specified")?; + + let sns_transaction_fee_e8s = self + .transaction_fee_e8s + .ok_or("Error: transaction_fee_e8s must be specified")?; + + // (2) + if min_direct_participation_icp_e8s == 0 { + return Err("Error: min_direct_participation_icp_e8s must be > 0".to_string()); + } + if min_participant_icp_e8s == 0 { + return Err("Error: min_participant_icp_e8s must be > 0".to_string()); + } + if min_participants == 0 { + return Err("Error: min_participants must be > 0".to_string()); + } + // Needed as the SwapInit min_participants field is a `u32`. + if min_participants > (u32::MAX as u64) { + return Err(format!( + "Error: min_participants cannot be greater than {}", + u32::MAX + )); + } + + // (3) + if max_direct_participation_icp_e8s < min_direct_participation_icp_e8s { + return Err(format!( + "Error: max_direct_participation_icp_e8s ({}) \ + must be >= min_direct_participation_icp_e8s ({})", + max_direct_participation_icp_e8s, min_direct_participation_icp_e8s + )); + } + if max_participant_icp_e8s < min_participant_icp_e8s { + return Err(format!( + "Error: max_participant_icp_e8s ({}) must be >= min_participant_icp_e8s ({})", + max_participant_icp_e8s, min_participant_icp_e8s + )); + } + + // (4) + if max_participant_icp_e8s > max_direct_participation_icp_e8s { + return Err(format!( + "Error: max_participant_icp_e8s ({}) \ + must be <= max_direct_participation_icp_e8s ({})", + max_participant_icp_e8s, max_direct_participation_icp_e8s + )); + } + + // (5) + if max_direct_participation_icp_e8s > MAX_DIRECT_ICP_CONTRIBUTION_TO_SWAP { + return Err(format!( + "Error: max_direct_participation_icp_e8s ({}) can be at most {} ICP E8s", + max_direct_participation_icp_e8s, MAX_DIRECT_ICP_CONTRIBUTION_TO_SWAP + )); + } + + // (6) + if max_direct_participation_icp_e8s + < min_participants.saturating_mul(min_participant_icp_e8s) + { + return Err(format!( + "Error: max_direct_participation_icp_e8s ({}) \ + must be >= min_participants ({}) * min_participant_icp_e8s ({})", + max_direct_participation_icp_e8s, min_participants, min_participant_icp_e8s + )); + } + + // (7) + if neuron_minimum_stake_e8s <= sns_transaction_fee_e8s { + return Err(format!( + "Error: neuron_minimum_stake_e8s={} is too small. It needs to be \ + greater than the transaction fee ({} e8s)", + neuron_minimum_stake_e8s, sns_transaction_fee_e8s + )); + } + + // (8) + let min_participant_sns_e8s = min_participant_icp_e8s as u128 + * initial_swap_amount_e8s as u128 + / max_direct_participation_icp_e8s as u128; + + let min_participant_icp_e8s_big_enough = min_participant_sns_e8s + >= neuron_basket_construction_parameters_count as u128 + * (neuron_minimum_stake_e8s + sns_transaction_fee_e8s) as u128; + + if !min_participant_icp_e8s_big_enough { + return Err(format!( + "Error: min_participant_icp_e8s={} is too small. It needs to be \ + large enough to ensure that participants will end up with \ + enough SNS tokens to form {} SNS neurons, each of which \ + require at least {} SNS e8s, plus {} e8s in transaction \ + fees. More precisely, the following inequality must hold: \ + min_participant_icp_e8s >= neuron_basket_count \ + * (neuron_minimum_stake_e8s + transaction_fee_e8s) \ + * max_direct_participation_icp_e8s / initial_swap_amount_e8s", + min_participant_icp_e8s, + neuron_basket_construction_parameters_count, + neuron_minimum_stake_e8s, + sns_transaction_fee_e8s, + )); + } + + // (9) + // Conceptually, we want to calculate the following value: + // ``` + // let max_sns_neurons_for_direct_participants = { + // let max_participants = max_direct_participation_icp_e8s / min_participant_icp_e8s; + // max_participants * neuron_basket_construction_parameters_count; + // }; + // ``` + // To minimize rounding errors related to integer division, we first do `*` and then `/`. + let max_sns_neurons_for_direct_participants = max_direct_participation_icp_e8s as u128 + * neuron_basket_construction_parameters_count as u128 + / min_participant_icp_e8s as u128; + if max_sns_neurons_for_direct_participants > MAX_NEURONS_FOR_DIRECT_PARTICIPANTS as u128 { + return Err(format!( + "Error: The number of SNS neurons created for direct participants of a successful \ + swap ((max_direct_participation_icp_e8s={}) \ + * (neuron_basket_construction_parameters_count={}) \ + / (min_participant_icp_e8s={}) = {}) must not exceed \ + (MAX_NEURONS_FOR_DIRECT_PARTICIPANTS={}).", + max_direct_participation_icp_e8s, + neuron_basket_construction_parameters_count, + min_participant_icp_e8s, + max_sns_neurons_for_direct_participants, + MAX_NEURONS_FOR_DIRECT_PARTICIPANTS + )); + } + + Ok(()) + } + + fn validate_nns_proposal_id_pre_execution(&self) -> Result<(), String> { + if self.nns_proposal_id.is_none() { + Ok(()) + } else { + Err(format!( + "Error: nns_proposal_id cannot be specified pre_execution, but was {:?}", + self.nns_proposal_id + )) + } + } + + fn validate_nns_proposal_id(&self) -> Result<(), String> { + match self.nns_proposal_id { + None => Err("Error: nns_proposal_id must be specified".to_string()), + Some(_) => Ok(()), + } + } + + fn validate_neurons_fund_participants_pre_execution(&self) -> Result<(), String> { + if self.neurons_fund_participants.is_none() { + Ok(()) + } else { + Err(format!( + "Error: neurons_fund_participants cannot be specified pre_execution, but was {:?}", + self.neurons_fund_participants + )) + } + } + + fn validate_neurons_fund_participants(&self) -> Result<(), String> { + if self.neurons_fund_participants.is_none() { + Ok(()) + } else { + Err(format!( + "Error: neurons_fund_participants can be set only by Swap; was initialized to {:?}", + self.neurons_fund_participants + )) + } + } + + fn validate_swap_start_timestamp_seconds_pre_execution(&self) -> Result<(), String> { + if self.swap_start_timestamp_seconds.is_none() { + Ok(()) + } else { + Err(format!( + "Error: swap_start_timestamp_seconds cannot be specified pre_execution, but was {:?}", + self.swap_start_timestamp_seconds + )) + } + } + + fn validate_swap_start_timestamp_seconds(&self) -> Result<(), String> { + match self.swap_start_timestamp_seconds { + Some(_) => Ok(()), + None => Err("Error: swap_start_timestamp_seconds must be specified".to_string()), + } + } + + fn validate_swap_due_timestamp_seconds_pre_execution(&self) -> Result<(), String> { + if self.swap_due_timestamp_seconds.is_none() { + Ok(()) + } else { + Err(format!( + "Error: swap_due_timestamp_seconds cannot be specified pre_execution, but was {:?}", + self.swap_due_timestamp_seconds + )) + } + } + + fn validate_swap_due_timestamp_seconds(&self) -> Result<(), String> { + let swap_start_timestamp_seconds = self + .swap_start_timestamp_seconds + .ok_or("Error: swap_start_timestamp_seconds must be specified")?; + + let swap_due_timestamp_seconds = self + .swap_due_timestamp_seconds + .ok_or("Error: swap_due_timestamp_seconds must be specified")?; + + if swap_due_timestamp_seconds < swap_start_timestamp_seconds { + return Err(format!( + "Error: swap_due_timestamp_seconds({}) must be after swap_start_timestamp_seconds({})", + swap_due_timestamp_seconds, swap_start_timestamp_seconds, + )); + } + + Ok(()) + } + + pub fn validate_neurons_fund_participation(&self) -> Result<(), String> { + if self.neurons_fund_participation.is_none() { + return Err("SnsInitPayload.neurons_fund_participation must be specified".into()); + } + Ok(()) + } + + pub fn validate_neurons_fund_participation_constraints( + &self, + is_pre_execution: bool, + ) -> Result<(), String> { + // This field must be set by NNS Governance at proposal execution time, not before. + // This check will also catch the situation in which we are in the legacy (pre-1-prop) flow, + // in which the `neurons_fund_participation_constraints`` field must not be set at all. + if is_pre_execution && self.neurons_fund_participation_constraints.is_some() { + return Result::from( + NeuronsFundParticipationConstraintsValidationError::SetBeforeProposalExecution, + ); + } + + let Some(ref neurons_fund_participation_constraints) = + self.neurons_fund_participation_constraints + else { + if self.neurons_fund_participation == Some(true) && !is_pre_execution { + return Result::from(NeuronsFundParticipationConstraintsValidationError::RelatedFieldUnspecified( + "neurons_fund_participation requires neurons_fund_participation_constraints" + .to_string(), + )); + } + return Ok(()); + }; + + // Validate relationship with min_direct_participation_threshold_icp_e8s + let Some(min_direct_participation_threshold_icp_e8s) = + neurons_fund_participation_constraints.min_direct_participation_threshold_icp_e8s + else { + return Result::from(NeuronsFundParticipationConstraintsValidationError::MinDirectParticipationThresholdValidationError( + MinDirectParticipationThresholdValidationError::Unspecified + )); + }; + + let min_direct_participation_icp_e8s = + self.min_direct_participation_icp_e8s.ok_or_else(|| { + NeuronsFundParticipationConstraintsValidationError::RelatedFieldUnspecified( + "min_direct_participation_icp_e8s".to_string(), + ) + .to_string() + })?; + if min_direct_participation_threshold_icp_e8s < min_direct_participation_icp_e8s { + return Result::from(NeuronsFundParticipationConstraintsValidationError::MinDirectParticipationThresholdValidationError( + MinDirectParticipationThresholdValidationError::BelowSwapDirectIcpMin { + min_direct_participation_threshold_icp_e8s, + min_direct_participation_icp_e8s, + } + )); + } + let max_direct_participation_icp_e8s = + self.max_direct_participation_icp_e8s.ok_or_else(|| { + NeuronsFundParticipationConstraintsValidationError::RelatedFieldUnspecified( + "max_direct_participation_icp_e8s".to_string(), + ) + .to_string() + })?; + if min_direct_participation_threshold_icp_e8s > max_direct_participation_icp_e8s { + return Result::from(NeuronsFundParticipationConstraintsValidationError::MinDirectParticipationThresholdValidationError( + MinDirectParticipationThresholdValidationError::AboveSwapDirectIcpMax { + min_direct_participation_threshold_icp_e8s, + max_direct_participation_icp_e8s, + } + )); + } + + // Validate relationship with max_neurons_fund_participation_icp_e8s + let Some(max_neurons_fund_participation_icp_e8s) = + neurons_fund_participation_constraints.max_neurons_fund_participation_icp_e8s + else { + return Result::from(NeuronsFundParticipationConstraintsValidationError::MaxNeuronsFundParticipationValidationError( + MaxNeuronsFundParticipationValidationError::Unspecified + )); + }; + + let min_participant_icp_e8s = self.min_participant_icp_e8s.ok_or_else(|| { + NeuronsFundParticipationConstraintsValidationError::RelatedFieldUnspecified( + "min_participant_icp_e8s".to_string(), + ) + .to_string() + })?; + if 0 < max_neurons_fund_participation_icp_e8s + && max_neurons_fund_participation_icp_e8s < min_participant_icp_e8s + { + let max_neurons_fund_participation_icp_e8s = + NonZeroU64::new(max_neurons_fund_participation_icp_e8s).unwrap(); + return Result::from(NeuronsFundParticipationConstraintsValidationError::MaxNeuronsFundParticipationValidationError( + MaxNeuronsFundParticipationValidationError::BelowSingleParticipationLimit { + max_neurons_fund_participation_icp_e8s, + min_participant_icp_e8s, + } + )); + } + // Not more than 50% of total contributions should come from the Neurons' Fund. + let max_direct_participation_icp_e8s = + self.max_direct_participation_icp_e8s.ok_or_else(|| { + NeuronsFundParticipationConstraintsValidationError::RelatedFieldUnspecified( + "max_direct_participation_icp_e8s".to_string(), + ) + .to_string() + })?; + if max_neurons_fund_participation_icp_e8s > max_direct_participation_icp_e8s { + return Result::from(NeuronsFundParticipationConstraintsValidationError::MaxNeuronsFundParticipationValidationError( + MaxNeuronsFundParticipationValidationError::AboveSwapMaxDirectIcp { + max_neurons_fund_participation_icp_e8s, + max_direct_participation_icp_e8s, + } + )); + } + + neurons_fund_participation_constraints + .validate() + .map_err(|err| { + NeuronsFundParticipationConstraintsValidationError::Local(err).to_string() + }) + } + + pub fn validate_all_post_execution_swap_parameters_are_set(&self) -> Result<(), String> { + let mut missing_one_proposal_fields = vec![]; + if self.nns_proposal_id.is_none() { + missing_one_proposal_fields.push("nns_proposal_id") + } + if self.swap_start_timestamp_seconds.is_none() { + missing_one_proposal_fields.push("swap_start_timestamp_seconds") + } + if self.swap_due_timestamp_seconds.is_none() { + missing_one_proposal_fields.push("swap_due_timestamp_seconds") + } + if self.min_direct_participation_icp_e8s.is_none() { + missing_one_proposal_fields.push("min_direct_participation_icp_e8s") + } + if self.max_direct_participation_icp_e8s.is_none() { + missing_one_proposal_fields.push("max_direct_participation_icp_e8s") + } + + if missing_one_proposal_fields.is_empty() { + Ok(()) + } else { + Err(format!( + "Error in validate_all_post_execution_swap_parameters_are_set: The one-proposal \ + SNS initialization requires some SnsInitPayload parameters to be Some. But the \ + following fields were set to None: {}", + missing_one_proposal_fields.join(", ") + )) + } + } + + pub fn validate_all_non_legacy_pre_execution_swap_parameters_are_set( + &self, + ) -> Result<(), String> { + let mut missing_one_proposal_fields = vec![]; + if self.min_participants.is_none() { + missing_one_proposal_fields.push("min_participants") + } + + if self.min_direct_participation_icp_e8s.is_none() { + missing_one_proposal_fields.push("min_direct_participation_icp_e8s") + } + + if self.max_direct_participation_icp_e8s.is_none() { + missing_one_proposal_fields.push("max_direct_participation_icp_e8s") + } + if self.min_participant_icp_e8s.is_none() { + missing_one_proposal_fields.push("min_participant_icp_e8s") + } + if self.max_participant_icp_e8s.is_none() { + missing_one_proposal_fields.push("max_participant_icp_e8s") + } + if self.neuron_basket_construction_parameters.is_none() { + missing_one_proposal_fields.push("neuron_basket_construction_parameters") + } + if self.dapp_canisters.is_none() { + missing_one_proposal_fields.push("dapp_canisters") + } + if self.token_logo.is_none() { + missing_one_proposal_fields.push("token_logo") + } + + if missing_one_proposal_fields.is_empty() { + Ok(()) + } else { + Err(format!( + "Error in validate_all_non_legacy_pre_execution_swap_parameters_are_set: The one-\ + proposal SNS initialization requires some SnsInitPayload parameters to be Some. \ + But the following fields were set to None: {}", + missing_one_proposal_fields.join(", ") + )) + } + } +} diff --git a/ssr/Cargo.toml b/ssr/Cargo.toml index 78ac35b8..069b3969 100644 --- a/ssr/Cargo.toml +++ b/ssr/Cargo.toml @@ -19,6 +19,7 @@ simple_logger = "4.0" tokio = { version = "1", optional = true, features = [ "rt-multi-thread", "signal", + "time", ] } tower = { version = "0.4", optional = true } tower-http = { version = "0.5", features = ["fs"], optional = true } @@ -26,12 +27,14 @@ wasm-bindgen = "=0.2.93" thiserror = "1.0" tracing = { version = "0.1.37", optional = true } http = "1.1.0" -serde = { version = "1.0", features = ["derive"] } -candid = "0.10.3" +serde.workspace = true +candid.workspace = true ic-agent = { version = "0.36.0", default-features = false, features = [ "pem", "reqwest", ] } +ic-base-types = { git = "https://github.com/dfinity/ic", rev = "tags/release-2024-05-29_23-02-base" } +icp-ledger = { git = "https://github.com/dfinity/ic", rev = "tags/release-2024-05-29_23-02-base" } serde-wasm-bindgen = "0.6.5" futures = "0.3.30" leptos-use = "0.12.0" @@ -40,13 +43,13 @@ reqwest = { version = "0.12", default-features = false, features = [ "json", "http2", ] } -serde_bytes = "0.11.14" +serde_bytes.workspace = true hex = "0.4.3" leptos_icons = "0.3.0" icondata = "0.3.0" gloo = { version = "0.11.0", features = ["futures", "net", "net"] } once_cell = "1.19.0" -web-time = "1.0.0" +web-time.workspace = true k256 = { version = "0.13.3", default-features = false, features = [ "std", "jwk", @@ -63,6 +66,7 @@ web-sys = { version = "0.3", features = [ "Window", "Document", "Worker", + "CanvasRenderingContext2d", ], optional = true } circular-buffer = "0.1.7" redb = { version = "2.0.0", optional = true } @@ -91,17 +95,30 @@ hmac = { version = "0.12.1", optional = true } wasm-bindgen-futures = { version = "0.4.42" } testcontainers = { version = "0.20.0", optional = true } yral-testcontainers = { git = "https://github.com/go-bazzinga/yral-testcontainers", rev = "f9d2c01c498d58fca0595a48bdc3f9400e57ec2f", optional = true } +sns-validation.workspace = true js-sys = "0.3.69" tonic-web-wasm-client = { version = "0.6" } -tonic = { version = "0.12.0", default-features = false, features = ["prost", "codegen"] } +tonic = { version = "0.12.0", default-features = false, features = [ + "prost", + "codegen", +] } prost = "0.13.0" +priority-queue = "2.1.0" +rust_decimal = "1.36" +firestore = { version = "0.43.1", default-features = false, features = [ + "tls-webpki-roots", +], optional = true } +speedate = { version = "0.14.4", optional = true } +urlencoding = "2.1.3" [build-dependencies] serde = { version = "1.0", features = ["derive"] } candid_parser = "0.1.1" serde_json = "1.0.110" convert_case = "0.6.0" -tonic-build = { version = "0.12.0", default-features = false, features = ["prost"] } +tonic-build = { version = "0.12.0", default-features = false, features = [ + "prost", +] } anyhow = "1.0.86" [features] @@ -141,7 +158,7 @@ ssr = [ "tonic/tls", "tonic/tls-webpki-roots", "tonic/transport", - "tonic-build/transport" + "tonic-build/transport", ] # Fetch mock referral history instead of history via canister mock-referral-history = ["dep:rand_chacha", "k256/arithmetic"] @@ -156,6 +173,7 @@ cloudflare = ["dep:gob-cloudflare"] backend-admin = [] ga4 = [] mock-wallet-history = ["dep:rand_chacha"] +firestore = ["dep:firestore", "speedate"] release-bin = [ "ssr", "cloudflare", @@ -164,6 +182,7 @@ release-bin = [ "backend-admin", "oauth-ssr", "ga4", + "firestore", ] release-lib = [ "hydrate", diff --git a/ssr/build.rs b/ssr/build.rs index 83198cc3..f523802f 100644 --- a/ssr/build.rs +++ b/ssr/build.rs @@ -69,10 +69,11 @@ mod build_common { } fn build_did_intf() -> Result<()> { - println!("cargo:rerun-if-changed=../did/*"); + println!("cargo:rerun-if-changed=./did/*"); let mut candid_config = candid_parser::bindings::rust::Config::new(); candid_config.set_target(candid_parser::bindings::rust::Target::Agent); + candid_config.set_type_attributes("#[derive(CandidType, Deserialize, Debug)]".into()); let mut did_mod_contents = String::new(); // create $OUT_DIR/did diff --git a/ssr/did/canister_ids.json b/ssr/did/canister_ids.json index bc34e721..cfa3f0a1 100644 --- a/ssr/did/canister_ids.json +++ b/ssr/did/canister_ids.json @@ -1,12 +1,4 @@ { - "configuration": { - "ic": "efsfj-sqaaa-aaaap-qatwa-cai", - "local": "bnz7o-iuaaa-aaaaa-qaaaa-cai" - }, - "data_backup": { - "ic": "jwktp-qyaaa-aaaag-abcfa-cai", - "local": "bkyz2-fmaaa-aaaaa-qaaaq-cai" - }, "individual_user_template": { "ic": "dc47w-kaaaa-aaaak-qav3q-cai", "local": "bd3sg-teaaa-aaaaa-qaaba-cai" diff --git a/ssr/did/individual_user_template.did b/ssr/did/individual_user_template.did index 94891fb9..03cf60d9 100644 --- a/ssr/did/individual_user_template.did +++ b/ssr/did/individual_user_template.did @@ -3,6 +3,7 @@ type AggregateStats = record { total_amount_bet : nat64; total_number_of_hot_bets : nat64; }; +type AirdropDistribution = record { airdrop_neurons : vec NeuronDistribution }; type BetDetails = record { bet_direction : BetDirection; bet_maker_canister_id : principal; @@ -36,6 +37,49 @@ type BettingStatus = variant { }; BettingClosed; }; +type Canister = record { id : opt principal }; +type CdaoDeployError = variant { + CycleError : text; + Unregistered; + CallError : record { RejectionCode; text }; + InvalidInitPayload : text; + TokenLimit : nat64; + Unauthenticated; +}; +type CdaoTokenError = variant { + NoBalance; + InvalidRoot; + CallError : record { RejectionCode; text }; + Transfer : TransferError; + Unauthenticated; +}; +type CfNeuron = record { + has_created_neuron_recipes : opt bool; + nns_neuron_id : nat64; + amount_icp_e8s : nat64; +}; +type CfParticipant = record { + hotkey_principal : text; + cf_neurons : vec CfNeuron; +}; +type Committed = record { + total_direct_participation_icp_e8s : opt nat64; + total_neurons_fund_participation_icp_e8s : opt nat64; + sns_governance_canister_id : opt principal; +}; +type Countries = record { iso_codes : vec text }; +type DappCanisters = record { canisters : vec Canister }; +type DeployedCdaoCanisters = record { + root : principal; + swap : principal; + ledger : principal; + index : principal; + governance : principal; +}; +type DeveloperDistribution = record { + developer_neurons : vec NeuronDistribution; +}; +type DeviceIdentity = record { device_id : text; timestamp : nat64 }; type FeedScore = record { current_score : nat64; last_synchronized_at : SystemTime; @@ -60,11 +104,18 @@ type FollowerArg = record { follower_canister_id : principal; follower_principal_id : principal; }; +type FractionalDeveloperVotingPower = record { + treasury_distribution : opt TreasuryDistribution; + developer_distribution : opt DeveloperDistribution; + airdrop_distribution : opt AirdropDistribution; + swap_distribution : opt SwapDistribution; +}; type GetPostsOfUserProfileError = variant { ReachedEndOfItemsList; InvalidBoundsPassed; ExceededMaxNumberOfItemsAllowedInOneRequest; }; +type GovernanceError = record { error_message : text; error_type : int32 }; type HotOrNotDetails = record { hot_or_not_feed_score : FeedScore; aggregate_stats : AggregateStats; @@ -90,14 +141,17 @@ type HotOrNotOutcomePayoutEvent = variant { type HttpRequest = record { url : text; method : text; - body : vec nat8; + body : blob; headers : vec record { text; text }; }; type HttpResponse = record { - body : vec nat8; + body : blob; headers : vec record { text; text }; status_code : nat16; }; +type IdealMatchedParticipationFunction = record { + serialized_representation : opt text; +}; type IndividualUserTemplateInitArgs = record { known_principal_ids : opt vec record { KnownPrincipalType; principal }; version : text; @@ -105,6 +159,9 @@ type IndividualUserTemplateInitArgs = record { profile_owner : opt principal; upgrade_version_number : opt nat64; }; +type InitialTokenDistribution = variant { + FractionalDeveloperVotingPower : FractionalDeveloperVotingPower; +}; type KnownPrincipalType = variant { CanisterIdUserIndex; CanisterIdPlatformOrchestrator; @@ -114,18 +171,34 @@ type KnownPrincipalType = variant { CanisterIdTopicCacheIndex; CanisterIdRootCanister; CanisterIdDataBackup; + CanisterIdSnsWasm; CanisterIdPostCache; CanisterIdSNSController; CanisterIdSnsGovernance; UserIdGlobalSuperAdmin; }; +type LinearScalingCoefficient = record { + slope_numerator : opt nat64; + intercept_icp_e8s : opt nat64; + from_direct_participation_icp_e8s : opt nat64; + slope_denominator : opt nat64; + to_direct_participation_icp_e8s : opt nat64; +}; +type MLFeedCacheItem = record { + post_id : nat64; + canister_id : principal; + video_id : text; + creator_principal_id : opt principal; +}; type MigrationErrors = variant { InvalidToCanister; InvalidFromCanister; MigrationInfoNotFound; UserNotRegistered; + RequestCycleFromUserIndexFailed : text; + UserIndexCanisterIdNotFound; Unauthorized; - TransferToCanisterCallFailed; + TransferToCanisterCallFailed : text; HotOrNotSubnetCanisterIdNotFound; AlreadyUsedForMigration; CanisterInfoFailed; @@ -143,6 +216,47 @@ type MintEvent = variant { referee_user_principal_id : principal; }; }; +type NamespaceErrors = variant { + UserNotSignedUp; + ValueTooBig; + NamespaceNotFound; + Unauthorized; +}; +type NamespaceForFrontend = record { + id : nat64; + title : text; + owner_id : principal; +}; +type NeuronBasketConstructionParameters = record { + dissolve_delay_interval_seconds : nat64; + count : nat64; +}; +type NeuronDistribution = record { + controller : opt principal; + dissolve_delay_seconds : nat64; + memo : nat64; + stake_e8s : nat64; + vesting_period_seconds : opt nat64; +}; +type NeuronsFundNeuron = record { + hotkey_principal : opt text; + is_capped : opt bool; + nns_neuron_id : opt nat64; + amount_icp_e8s : opt nat64; +}; +type NeuronsFundParticipants = record { participants : vec CfParticipant }; +type NeuronsFundParticipationConstraints = record { + coefficient_intervals : vec LinearScalingCoefficient; + max_neurons_fund_participation_icp_e8s : opt nat64; + min_direct_participation_threshold_icp_e8s : opt nat64; + ideal_matched_participation_function : opt IdealMatchedParticipationFunction; +}; +type Ok = record { neurons_fund_neuron_portions : vec NeuronsFundNeuron }; +type PaginationError = variant { + ReachedEndOfItemsList; + InvalidBoundsPassed; + ExceededMaxNumberOfItemsAllowedInOneRequest; +}; type PlaceBetArg = record { bet_amount : nat64; post_id : nat64; @@ -221,31 +335,54 @@ type PostViewStatistics = record { average_watch_percentage : nat8; threshold_view_count : nat64; }; -type Result = variant { Ok : nat64; Err : text }; -type Result_1 = variant { - Ok : BettingStatus; - Err : BetOnCurrentlyViewingPostError; -}; -type Result_10 = variant { - Ok : UserProfileDetailsForFrontend; - Err : UpdateProfileDetailsError; -}; -type Result_11 = variant { Ok; Err : text }; -type Result_12 = variant { Ok; Err : UpdateProfileSetUniqueUsernameError }; -type Result_2 = variant { Ok : bool; Err : FollowAnotherUserProfileError }; -type Result_3 = variant { Ok : Post; Err }; -type Result_4 = variant { Ok : SystemTime; Err : text }; -type Result_5 = variant { +type RejectionCode = variant { + NoError; + CanisterError; + SysTransient; + DestinationInvalid; + Unknown; + SysFatal; + CanisterReject; +}; +type Result = variant { Ok : bool; Err : text }; +type Result_1 = variant { Ok : nat64; Err : text }; +type Result_10 = variant { Ok : SystemTime; Err : text }; +type Result_11 = variant { Ok : vec PostDetailsForFrontend; Err : GetPostsOfUserProfileError; }; -type Result_6 = variant { Ok : SessionType; Err : text }; -type Result_7 = variant { +type Result_12 = variant { Ok : SessionType; Err : text }; +type Result_13 = variant { Ok : vec SuccessHistoryItemV1; Err : text }; +type Result_14 = variant { Ok : vec principal; Err : PaginationError }; +type Result_15 = variant { Ok : vec record { nat64; TokenEvent }; - Err : GetPostsOfUserProfileError; + Err : PaginationError; +}; +type Result_16 = variant { Ok : vec WatchHistoryItem; Err : text }; +type Result_17 = variant { Ok : vec text; Err : NamespaceErrors }; +type Result_18 = variant { Ok : vec record { nat64; nat8 }; Err : text }; +type Result_19 = variant { Ok; Err : MigrationErrors }; +type Result_2 = variant { Ok : bool; Err : CdaoTokenError }; +type Result_20 = variant { Committed : Committed; Aborted : record {} }; +type Result_21 = variant { Ok : Ok; Err : GovernanceError }; +type Result_22 = variant { Ok; Err : CdaoTokenError }; +type Result_23 = variant { Ok : text; Err : text }; +type Result_24 = variant { + Ok : UserProfileDetailsForFrontend; + Err : UpdateProfileDetailsError; +}; +type Result_25 = variant { Ok; Err : text }; +type Result_26 = variant { Ok; Err : UpdateProfileSetUniqueUsernameError }; +type Result_3 = variant { + Ok : BettingStatus; + Err : BetOnCurrentlyViewingPostError; }; -type Result_8 = variant { Ok; Err : MigrationErrors }; -type Result_9 = variant { Ok : text; Err : text }; +type Result_4 = variant { Ok : NamespaceForFrontend; Err : NamespaceErrors }; +type Result_5 = variant { Ok : opt text; Err : NamespaceErrors }; +type Result_6 = variant { Ok; Err : NamespaceErrors }; +type Result_7 = variant { Ok : DeployedCdaoCanisters; Err : CdaoDeployError }; +type Result_8 = variant { Ok : bool; Err : FollowAnotherUserProfileError }; +type Result_9 = variant { Ok : Post; Err }; type RoomBetPossibleOutcomes = variant { HotWon; BetOngoing; Draw; NotWon }; type RoomDetails = record { total_hot_bets : nat64; @@ -255,8 +392,66 @@ type RoomDetails = record { bet_outcome : RoomBetPossibleOutcomes; }; type SessionType = variant { AnonymousSession; RegisteredSession }; +type SettleNeuronsFundParticipationRequest = record { + result : opt Result_20; + nns_proposal_id : opt nat64; +}; +type SettleNeuronsFundParticipationResponse = record { result : opt Result_21 }; type SlotDetails = record { room_details : vec record { nat64; RoomDetails } }; +type SnsInitPayload = record { + url : opt text; + max_dissolve_delay_seconds : opt nat64; + max_dissolve_delay_bonus_percentage : opt nat64; + nns_proposal_id : opt nat64; + neurons_fund_participation : opt bool; + min_participant_icp_e8s : opt nat64; + neuron_basket_construction_parameters : opt NeuronBasketConstructionParameters; + fallback_controller_principal_ids : vec text; + token_symbol : opt text; + final_reward_rate_basis_points : opt nat64; + max_icp_e8s : opt nat64; + neuron_minimum_stake_e8s : opt nat64; + confirmation_text : opt text; + logo : opt text; + name : opt text; + swap_start_timestamp_seconds : opt nat64; + swap_due_timestamp_seconds : opt nat64; + initial_voting_period_seconds : opt nat64; + neuron_minimum_dissolve_delay_to_vote_seconds : opt nat64; + description : opt text; + max_neuron_age_seconds_for_age_bonus : opt nat64; + min_participants : opt nat64; + initial_reward_rate_basis_points : opt nat64; + wait_for_quiet_deadline_increase_seconds : opt nat64; + transaction_fee_e8s : opt nat64; + dapp_canisters : opt DappCanisters; + neurons_fund_participation_constraints : opt NeuronsFundParticipationConstraints; + neurons_fund_participants : opt NeuronsFundParticipants; + max_age_bonus_percentage : opt nat64; + initial_token_distribution : opt InitialTokenDistribution; + reward_rate_transition_duration_seconds : opt nat64; + token_logo : opt text; + token_name : opt text; + max_participant_icp_e8s : opt nat64; + min_direct_participation_icp_e8s : opt nat64; + proposal_reject_cost_e8s : opt nat64; + restricted_countries : opt Countries; + min_icp_e8s : opt nat64; + max_direct_participation_icp_e8s : opt nat64; +}; type StakeEvent = variant { BetOnHotOrNotPost : PlaceBetArg }; +type SuccessHistoryItemV1 = record { + post_id : nat64; + percentage_watched : float32; + item_type : text; + publisher_canister_id : principal; + cf_video_id : text; + interacted_at : SystemTime; +}; +type SwapDistribution = record { + total_e8s : nat64; + initial_swap_amount_e8s : nat64; +}; type SystemTime = record { nanos_since_epoch : nat32; secs_since_epoch : nat64; @@ -285,6 +480,17 @@ type TokenEvent = variant { amount : nat64; }; }; +type TransferError = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + BadBurn : record { min_burn_amount : nat }; + Duplicate : record { duplicate_of : nat }; + BadFee : record { expected_fee : nat }; + CreatedInFuture : record { ledger_time : nat64 }; + TooOld; + InsufficientFunds : record { balance : nat }; +}; +type TreasuryDistribution = record { total_e8s : nat64 }; type UpdateProfileDetailsError = variant { NotAuthorized }; type UpdateProfileSetUniqueUsernameError = variant { UsernameAlreadyTaken; @@ -297,14 +503,6 @@ type UserCanisterDetails = record { user_canister_id : principal; profile_owner : principal; }; -type UserProfile = record { - unique_user_name : opt text; - profile_picture_url : opt text; - display_name : opt text; - principal_id : opt principal; - profile_stats : UserProfileGlobalStats; - referrer_details : opt UserCanisterDetails; -}; type UserProfileDetailsForFrontend = record { unique_user_name : opt text; lifetime_earnings : nat64; @@ -336,17 +534,31 @@ type UserProfileUpdateDetailsFromFrontend = record { profile_picture_url : opt text; display_name : opt text; }; +type WatchHistoryItem = record { + post_id : nat64; + viewed_at : SystemTime; + percentage_watched : float32; + publisher_canister_id : principal; + cf_video_id : text; +}; service : (IndividualUserTemplateInitArgs) -> { - add_post_v2 : (PostDetailsFromFrontend) -> (Result); - backup_data_to_backup_canister : (principal, principal) -> (); - bet_on_currently_viewing_post : (PlaceBetArg) -> (Result_1); + add_device_id : (text) -> (Result); + add_post_v2 : (PostDetailsFromFrontend) -> (Result_1); + add_token : (principal) -> (Result_2); + bet_on_currently_viewing_post : (PlaceBetArg) -> (Result_3); check_and_update_scores_and_share_with_post_cache_if_difference_beyond_threshold : ( vec nat64, ) -> (); clear_snapshot : () -> (); - do_i_follow_this_user : (FolloweeArg) -> (Result_2) query; - download_snapshot : (nat64, nat64) -> (vec nat8) query; - get_entire_individual_post_detail_by_id : (nat64) -> (Result_3) query; + create_a_namespace : (text) -> (Result_4); + delete_key_value_pair : (nat64, text) -> (Result_5); + delete_multiple_key_value_pairs : (nat64, vec text) -> (Result_6); + deploy_cdao_sns : (SnsInitPayload, nat64) -> (Result_7); + deployed_cdao_canisters : () -> (vec DeployedCdaoCanisters) query; + do_i_follow_this_user : (FolloweeArg) -> (Result_8) query; + download_snapshot : (nat64, nat64) -> (blob) query; + get_device_identities : () -> (vec DeviceIdentity) query; + get_entire_individual_post_detail_by_id : (nat64) -> (Result_9) query; get_hot_or_not_bet_details_for_this_post : (nat64) -> (BettingStatus) query; get_hot_or_not_bets_placed_by_this_profile_with_pagination : (nat64) -> ( vec PlacedBetDetail, @@ -355,13 +567,14 @@ service : (IndividualUserTemplateInitArgs) -> { opt PlacedBetDetail, ) query; get_individual_post_details_by_id : (nat64) -> (PostDetailsForFrontend) query; - get_last_access_time : () -> (Result_4) query; - get_last_canister_functionality_access_time : () -> (Result_4) query; + get_last_access_time : () -> (Result_10) query; + get_last_canister_functionality_access_time : () -> (Result_10) query; + get_ml_feed_cache_paginated : (nat64, nat64) -> (vec MLFeedCacheItem) query; get_posts_of_this_user_profile_with_pagination : (nat64, nat64) -> ( - Result_5, + Result_11, ) query; get_posts_of_this_user_profile_with_pagination_cursor : (nat64, nat64) -> ( - Result_5, + Result_11, ) query; get_principals_that_follow_this_profile_paginated : (opt nat64) -> ( vec record { nat64; FollowEntryDetail }, @@ -373,60 +586,69 @@ service : (IndividualUserTemplateInitArgs) -> { get_profile_details_v2 : () -> (UserProfileDetailsForFrontendV2) query; get_rewarded_for_referral : (principal, principal) -> (); get_rewarded_for_signing_up : () -> (); - get_session_type : () -> (Result_6) query; - get_stable_memory_size : () -> (nat32) query; + get_session_type : () -> (Result_12) query; + get_stable_memory_size : () -> (nat64) query; + get_success_history : () -> (Result_13) query; + get_token_roots_of_this_user_with_pagination_cursor : (nat64, nat64) -> ( + Result_14, + ) query; get_user_caniser_cycle_balance : () -> (nat) query; get_user_utility_token_transaction_history_with_pagination : ( nat64, nat64, - ) -> (Result_7) query; + ) -> (Result_15) query; get_utility_token_balance : () -> (nat64) query; get_version : () -> (text) query; get_version_number : () -> (nat64) query; + get_watch_history : () -> (Result_16) query; get_well_known_principal_value : (KnownPrincipalType) -> ( opt principal, ) query; http_request : (HttpRequest) -> (HttpResponse) query; + list_namespace_keys : (nat64) -> (Result_17) query; + list_namespaces : (nat64, nat64) -> (vec NamespaceForFrontend) query; load_snapshot : (nat64) -> (); - receive_and_save_snaphot : (nat64, vec nat8) -> (); - receive_bet_from_bet_makers_canister : (PlaceBetArg, principal) -> (Result_1); + once_reenqueue_timers_for_pending_bet_outcomes : () -> (Result_18); + read_key_value_pair : (nat64, text) -> (Result_5) query; + receive_and_save_snaphot : (nat64, blob) -> (); + receive_bet_from_bet_makers_canister : (PlaceBetArg, principal) -> (Result_3); receive_bet_winnings_when_distributed : (nat64, BetOutcomeForBetMaker) -> (); - receive_data_from_hotornot : ( - principal, - nat64, - vec record { nat64; Post }, - ) -> (Result_8); - receive_my_created_posts_from_data_backup_canister : (vec Post) -> (); - receive_my_profile_from_data_backup_canister : (UserProfile) -> (); - receive_my_utility_token_balance_from_data_backup_canister : (nat64) -> (); - receive_my_utility_token_transaction_history_from_data_backup_canister : ( - vec record { nat64; TokenEvent }, - ) -> (); - receive_principals_i_follow_from_data_backup_canister : (vec principal) -> (); - receive_principals_that_follow_me_from_data_backup_canister : ( - vec principal, - ) -> (); + receive_data_from_hotornot : (principal, nat64, vec Post) -> (Result_19); return_cycles_to_user_index_canister : (opt nat) -> (); save_snapshot_json : () -> (nat32); - transfer_tokens_and_posts : (principal, principal) -> (Result_8); - update_last_access_time : () -> (Result_9); + settle_neurons_fund_participation : ( + SettleNeuronsFundParticipationRequest, + ) -> (SettleNeuronsFundParticipationResponse); + transfer_token_to_user_canister : (principal, principal, opt blob, nat) -> ( + Result_22, + ); + transfer_tokens_and_posts : (principal, principal) -> (Result_19); + update_last_access_time : () -> (Result_23); update_last_canister_functionality_access_time : () -> (); + update_ml_feed_cache : (vec MLFeedCacheItem) -> (Result_23); update_post_add_view_details : (nat64, PostViewDetailsFromFrontend) -> (); update_post_as_ready_to_view : (nat64) -> (); update_post_increment_share_count : (nat64) -> (nat64); + update_post_status : (nat64, PostStatus) -> (); update_post_toggle_like_status_by_caller : (nat64) -> (bool); update_profile_display_details : (UserProfileUpdateDetailsFromFrontend) -> ( - Result_10, + Result_24, ); - update_profile_owner : (opt principal) -> (Result_11); - update_profile_set_unique_username_once : (text) -> (Result_12); + update_profile_owner : (opt principal) -> (Result_25); + update_profile_set_unique_username_once : (text) -> (Result_26); update_profiles_i_follow_toggle_list_with_specified_profile : ( FolloweeArg, - ) -> (Result_2); + ) -> (Result_8); update_profiles_that_follow_me_toggle_list_with_specified_profile : ( FollowerArg, - ) -> (Result_2); - update_referrer_details : (UserCanisterDetails) -> (Result_9); - update_session_type : (SessionType) -> (Result_9); + ) -> (Result_8); + update_referrer_details : (UserCanisterDetails) -> (Result_23); + update_session_type : (SessionType) -> (Result_23); + update_success_history : (SuccessHistoryItemV1) -> (Result_23); + update_watch_history : (WatchHistoryItem) -> (Result_23); update_well_known_principal : (KnownPrincipalType, principal) -> (); + write_key_value_pair : (nat64, text, text) -> (Result_5); + write_multiple_key_value_pairs : (nat64, vec record { text; text }) -> ( + Result_6, + ); } diff --git a/ssr/did/sns_governance.did b/ssr/did/sns_governance.did new file mode 100644 index 00000000..0231a105 --- /dev/null +++ b/ssr/did/sns_governance.did @@ -0,0 +1,480 @@ +type Account = record { owner : opt principal; subaccount : opt Subaccount }; +type Action = variant { + ManageNervousSystemParameters : NervousSystemParameters; + AddGenericNervousSystemFunction : NervousSystemFunction; + ManageDappCanisterSettings : ManageDappCanisterSettings; + RemoveGenericNervousSystemFunction : nat64; + UpgradeSnsToNextVersion : record {}; + RegisterDappCanisters : RegisterDappCanisters; + TransferSnsTreasuryFunds : TransferSnsTreasuryFunds; + UpgradeSnsControlledCanister : UpgradeSnsControlledCanister; + DeregisterDappCanisters : DeregisterDappCanisters; + MintSnsTokens : MintSnsTokens; + Unspecified : record {}; + ManageSnsMetadata : ManageSnsMetadata; + ExecuteGenericNervousSystemFunction : ExecuteGenericNervousSystemFunction; + ManageLedgerParameters : ManageLedgerParameters; + Motion : Motion; +}; +type ActionAuxiliary = variant { + TransferSnsTreasuryFunds : MintSnsTokensActionAuxiliary; + MintSnsTokens : MintSnsTokensActionAuxiliary; +}; +type AddNeuronPermissions = record { + permissions_to_add : opt NeuronPermissionList; + principal_id : opt principal; +}; +type Amount = record { e8s : nat64 }; +type Ballot = record { + vote : int32; + cast_timestamp_seconds : nat64; + voting_power : nat64; +}; +type By = variant { + MemoAndController : MemoAndController; + NeuronId : record {}; +}; +type CanisterStatusResultV2 = record { + status : CanisterStatusType; + memory_size : nat; + cycles : nat; + settings : DefiniteCanisterSettingsArgs; + idle_cycles_burned_per_day : nat; + module_hash : opt blob; +}; +type CanisterStatusType = variant { stopped; stopping; running }; +type ChangeAutoStakeMaturity = record { + requested_setting_for_auto_stake_maturity : bool; +}; +type ClaimOrRefresh = record { by : opt By }; +type ClaimOrRefreshResponse = record { refreshed_neuron_id : opt NeuronId }; +type ClaimSwapNeuronsRequest = record { + neuron_parameters : vec NeuronParameters; +}; +type ClaimSwapNeuronsResponse = record { + claim_swap_neurons_result : opt ClaimSwapNeuronsResult; +}; +type ClaimSwapNeuronsResult = variant { Ok : ClaimedSwapNeurons; Err : int32 }; +type ClaimedSwapNeurons = record { swap_neurons : vec SwapNeuron }; +type Command = variant { + Split : Split; + Follow : Follow; + DisburseMaturity : DisburseMaturity; + ClaimOrRefresh : ClaimOrRefresh; + Configure : Configure; + RegisterVote : RegisterVote; + MakeProposal : Proposal; + StakeMaturity : StakeMaturity; + RemoveNeuronPermissions : RemoveNeuronPermissions; + AddNeuronPermissions : AddNeuronPermissions; + MergeMaturity : MergeMaturity; + Disburse : Disburse; +}; +type Command_1 = variant { + Error : GovernanceError; + Split : SplitResponse; + Follow : record {}; + DisburseMaturity : DisburseMaturityResponse; + ClaimOrRefresh : ClaimOrRefreshResponse; + Configure : record {}; + RegisterVote : record {}; + MakeProposal : GetProposal; + RemoveNeuronPermission : record {}; + StakeMaturity : StakeMaturityResponse; + MergeMaturity : MergeMaturityResponse; + Disburse : DisburseResponse; + AddNeuronPermission : record {}; +}; +type Command_2 = variant { + Split : Split; + Follow : Follow; + DisburseMaturity : DisburseMaturity; + Configure : Configure; + RegisterVote : RegisterVote; + SyncCommand : record {}; + MakeProposal : Proposal; + FinalizeDisburseMaturity : FinalizeDisburseMaturity; + ClaimOrRefreshNeuron : ClaimOrRefresh; + RemoveNeuronPermissions : RemoveNeuronPermissions; + AddNeuronPermissions : AddNeuronPermissions; + MergeMaturity : MergeMaturity; + Disburse : Disburse; +}; +type Configure = record { operation : opt Operation }; +type Decimal = record { human_readable : opt text }; +type DefaultFollowees = record { followees : vec record { nat64; Followees } }; +type DefiniteCanisterSettingsArgs = record { + freezing_threshold : nat; + controllers : vec principal; + memory_allocation : nat; + compute_allocation : nat; +}; +type DeregisterDappCanisters = record { + canister_ids : vec principal; + new_controllers : vec principal; +}; +type Disburse = record { to_account : opt Account; amount : opt Amount }; +type DisburseMaturity = record { + to_account : opt Account; + percentage_to_disburse : nat32; +}; +type DisburseMaturityInProgress = record { + timestamp_of_disbursement_seconds : nat64; + amount_e8s : nat64; + account_to_disburse_to : opt Account; + finalize_disbursement_timestamp_seconds : opt nat64; +}; +type DisburseMaturityResponse = record { + amount_disbursed_e8s : nat64; + amount_deducted_e8s : opt nat64; +}; +type DisburseResponse = record { transfer_block_height : nat64 }; +type DissolveState = variant { + DissolveDelaySeconds : nat64; + WhenDissolvedTimestampSeconds : nat64; +}; +type ExecuteGenericNervousSystemFunction = record { + function_id : nat64; + payload : blob; +}; +type FinalizeDisburseMaturity = record { + amount_to_be_disbursed_e8s : nat64; + to_account : opt Account; +}; +type Follow = record { function_id : nat64; followees : vec NeuronId }; +type Followees = record { followees : vec NeuronId }; +type FunctionType = variant { + NativeNervousSystemFunction : record {}; + GenericNervousSystemFunction : GenericNervousSystemFunction; +}; +type GenericNervousSystemFunction = record { + validator_canister_id : opt principal; + target_canister_id : opt principal; + validator_method_name : opt text; + target_method_name : opt text; +}; +type GetMaturityModulationResponse = record { + maturity_modulation : opt MaturityModulation; +}; +type GetMetadataResponse = record { + url : opt text; + logo : opt text; + name : opt text; + description : opt text; +}; +type GetModeResponse = record { mode : opt int32 }; +type GetNeuron = record { neuron_id : opt NeuronId }; +type GetNeuronResponse = record { result : opt Result }; +type GetProposal = record { proposal_id : opt ProposalId }; +type GetProposalResponse = record { result : opt Result_1 }; +type GetRunningSnsVersionResponse = record { + deployed_version : opt Version; + pending_version : opt UpgradeInProgress; +}; +type GetSnsInitializationParametersResponse = record { + sns_initialization_parameters : text; +}; +type Governance = record { + root_canister_id : opt principal; + id_to_nervous_system_functions : vec record { nat64; NervousSystemFunction }; + metrics : opt GovernanceCachedMetrics; + maturity_modulation : opt MaturityModulation; + mode : int32; + parameters : opt NervousSystemParameters; + is_finalizing_disburse_maturity : opt bool; + deployed_version : opt Version; + sns_initialization_parameters : text; + latest_reward_event : opt RewardEvent; + pending_version : opt UpgradeInProgress; + swap_canister_id : opt principal; + ledger_canister_id : opt principal; + proposals : vec record { nat64; ProposalData }; + in_flight_commands : vec record { text; NeuronInFlightCommand }; + sns_metadata : opt ManageSnsMetadata; + neurons : vec record { text; Neuron }; + genesis_timestamp_seconds : nat64; +}; +type GovernanceCachedMetrics = record { + not_dissolving_neurons_e8s_buckets : vec record { nat64; float64 }; + garbage_collectable_neurons_count : nat64; + neurons_with_invalid_stake_count : nat64; + not_dissolving_neurons_count_buckets : vec record { nat64; nat64 }; + neurons_with_less_than_6_months_dissolve_delay_count : nat64; + dissolved_neurons_count : nat64; + total_staked_e8s : nat64; + total_supply_governance_tokens : nat64; + not_dissolving_neurons_count : nat64; + dissolved_neurons_e8s : nat64; + neurons_with_less_than_6_months_dissolve_delay_e8s : nat64; + dissolving_neurons_count_buckets : vec record { nat64; nat64 }; + dissolving_neurons_count : nat64; + dissolving_neurons_e8s_buckets : vec record { nat64; float64 }; + timestamp_seconds : nat64; +}; +type GovernanceError = record { error_message : text; error_type : int32 }; +type IncreaseDissolveDelay = record { + additional_dissolve_delay_seconds : nat32; +}; +type ListNervousSystemFunctionsResponse = record { + reserved_ids : vec nat64; + functions : vec NervousSystemFunction; +}; +type ListNeurons = record { + of_principal : opt principal; + limit : nat32; + start_page_at : opt NeuronId; +}; +type ListNeuronsResponse = record { neurons : vec Neuron }; +type ListProposals = record { + include_reward_status : vec int32; + before_proposal : opt ProposalId; + limit : nat32; + exclude_type : vec nat64; + include_status : vec int32; +}; +type ListProposalsResponse = record { + include_ballots_by_caller : opt bool; + proposals : vec ProposalData; +}; +type ManageDappCanisterSettings = record { + freezing_threshold : opt nat64; + canister_ids : vec principal; + reserved_cycles_limit : opt nat64; + log_visibility : opt int32; + memory_allocation : opt nat64; + compute_allocation : opt nat64; +}; +type ManageLedgerParameters = record { transfer_fee : opt nat64 }; +type ManageNeuron = record { subaccount : blob; command : opt Command }; +type ManageNeuronResponse = record { command : opt Command_1 }; +type ManageSnsMetadata = record { + url : opt text; + logo : opt text; + name : opt text; + description : opt text; +}; +type MaturityModulation = record { + current_basis_points : opt int32; + updated_at_timestamp_seconds : opt nat64; +}; +type MemoAndController = record { controller : opt principal; memo : nat64 }; +type MergeMaturity = record { percentage_to_merge : nat32 }; +type MergeMaturityResponse = record { + merged_maturity_e8s : nat64; + new_stake_e8s : nat64; +}; +type MintSnsTokens = record { + to_principal : opt principal; + to_subaccount : opt Subaccount; + memo : opt nat64; + amount_e8s : opt nat64; +}; +type MintSnsTokensActionAuxiliary = record { valuation : opt Valuation }; +type Motion = record { motion_text : text }; +type NervousSystemFunction = record { + id : nat64; + name : text; + description : opt text; + function_type : opt FunctionType; +}; +type NervousSystemParameters = record { + default_followees : opt DefaultFollowees; + max_dissolve_delay_seconds : opt nat64; + max_dissolve_delay_bonus_percentage : opt nat64; + max_followees_per_function : opt nat64; + neuron_claimer_permissions : opt NeuronPermissionList; + neuron_minimum_stake_e8s : opt nat64; + max_neuron_age_for_age_bonus : opt nat64; + initial_voting_period_seconds : opt nat64; + neuron_minimum_dissolve_delay_to_vote_seconds : opt nat64; + reject_cost_e8s : opt nat64; + max_proposals_to_keep_per_action : opt nat32; + wait_for_quiet_deadline_increase_seconds : opt nat64; + max_number_of_neurons : opt nat64; + transaction_fee_e8s : opt nat64; + max_number_of_proposals_with_ballots : opt nat64; + max_age_bonus_percentage : opt nat64; + neuron_grantable_permissions : opt NeuronPermissionList; + voting_rewards_parameters : opt VotingRewardsParameters; + maturity_modulation_disabled : opt bool; + max_number_of_principals_per_neuron : opt nat64; +}; +type Neuron = record { + id : opt NeuronId; + staked_maturity_e8s_equivalent : opt nat64; + permissions : vec NeuronPermission; + maturity_e8s_equivalent : nat64; + cached_neuron_stake_e8s : nat64; + created_timestamp_seconds : nat64; + source_nns_neuron_id : opt nat64; + auto_stake_maturity : opt bool; + aging_since_timestamp_seconds : nat64; + dissolve_state : opt DissolveState; + voting_power_percentage_multiplier : nat64; + vesting_period_seconds : opt nat64; + disburse_maturity_in_progress : vec DisburseMaturityInProgress; + followees : vec record { nat64; Followees }; + neuron_fees_e8s : nat64; +}; +type NeuronId = record { id : blob }; +type NeuronInFlightCommand = record { + command : opt Command_2; + timestamp : nat64; +}; +type NeuronParameters = record { + controller : opt principal; + dissolve_delay_seconds : opt nat64; + source_nns_neuron_id : opt nat64; + stake_e8s : opt nat64; + followees : vec NeuronId; + hotkey : opt principal; + neuron_id : opt NeuronId; +}; +type NeuronPermission = record { + "principal" : opt principal; + permission_type : vec int32; +}; +type NeuronPermissionList = record { permissions : vec int32 }; +type Operation = variant { + ChangeAutoStakeMaturity : ChangeAutoStakeMaturity; + StopDissolving : record {}; + StartDissolving : record {}; + IncreaseDissolveDelay : IncreaseDissolveDelay; + SetDissolveTimestamp : SetDissolveTimestamp; +}; +type Percentage = record { basis_points : opt nat64 }; +type Proposal = record { + url : text; + title : text; + action : opt Action; + summary : text; +}; +type ProposalData = record { + id : opt ProposalId; + payload_text_rendering : opt text; + action : nat64; + failure_reason : opt GovernanceError; + action_auxiliary : opt ActionAuxiliary; + ballots : vec record { text; Ballot }; + minimum_yes_proportion_of_total : opt Percentage; + reward_event_round : nat64; + failed_timestamp_seconds : nat64; + reward_event_end_timestamp_seconds : opt nat64; + proposal_creation_timestamp_seconds : nat64; + initial_voting_period_seconds : nat64; + reject_cost_e8s : nat64; + latest_tally : opt Tally; + wait_for_quiet_deadline_increase_seconds : nat64; + decided_timestamp_seconds : nat64; + proposal : opt Proposal; + proposer : opt NeuronId; + wait_for_quiet_state : opt WaitForQuietState; + minimum_yes_proportion_of_exercised : opt Percentage; + is_eligible_for_rewards : bool; + executed_timestamp_seconds : nat64; +}; +type ProposalId = record { id : nat64 }; +type RegisterDappCanisters = record { canister_ids : vec principal }; +type RegisterVote = record { vote : int32; proposal : opt ProposalId }; +type RemoveNeuronPermissions = record { + permissions_to_remove : opt NeuronPermissionList; + principal_id : opt principal; +}; +type Result = variant { Error : GovernanceError; Neuron : Neuron }; +type Result_1 = variant { Error : GovernanceError; Proposal : ProposalData }; +type RewardEvent = record { + rounds_since_last_distribution : opt nat64; + actual_timestamp_seconds : nat64; + end_timestamp_seconds : opt nat64; + total_available_e8s_equivalent : opt nat64; + distributed_e8s_equivalent : nat64; + round : nat64; + settled_proposals : vec ProposalId; +}; +type SetDissolveTimestamp = record { dissolve_timestamp_seconds : nat64 }; +type SetMode = record { mode : int32 }; +type Split = record { memo : nat64; amount_e8s : nat64 }; +type SplitResponse = record { created_neuron_id : opt NeuronId }; +type StakeMaturity = record { percentage_to_stake : opt nat32 }; +type StakeMaturityResponse = record { + maturity_e8s : nat64; + staked_maturity_e8s : nat64; +}; +type Subaccount = record { subaccount : blob }; +type SwapNeuron = record { id : opt NeuronId; status : int32 }; +type Tally = record { + no : nat64; + yes : nat64; + total : nat64; + timestamp_seconds : nat64; +}; +type Tokens = record { e8s : opt nat64 }; +type TransferSnsTreasuryFunds = record { + from_treasury : int32; + to_principal : opt principal; + to_subaccount : opt Subaccount; + memo : opt nat64; + amount_e8s : nat64; +}; +type UpgradeInProgress = record { + mark_failed_at_seconds : nat64; + checking_upgrade_lock : nat64; + proposal_id : nat64; + target_version : opt Version; +}; +type UpgradeSnsControlledCanister = record { + new_canister_wasm : blob; + mode : opt int32; + canister_id : opt principal; + canister_upgrade_arg : opt blob; +}; +type Valuation = record { + token : opt int32; + account : opt Account; + valuation_factors : opt ValuationFactors; + timestamp_seconds : opt nat64; +}; +type ValuationFactors = record { + xdrs_per_icp : opt Decimal; + icps_per_token : opt Decimal; + tokens : opt Tokens; +}; +type Version = record { + archive_wasm_hash : blob; + root_wasm_hash : blob; + swap_wasm_hash : blob; + ledger_wasm_hash : blob; + governance_wasm_hash : blob; + index_wasm_hash : blob; +}; +type VotingRewardsParameters = record { + final_reward_rate_basis_points : opt nat64; + initial_reward_rate_basis_points : opt nat64; + reward_rate_transition_duration_seconds : opt nat64; + round_duration_seconds : opt nat64; +}; +type WaitForQuietState = record { current_deadline_timestamp_seconds : nat64 }; +service : (Governance) -> { + claim_swap_neurons : (ClaimSwapNeuronsRequest) -> (ClaimSwapNeuronsResponse); + fail_stuck_upgrade_in_progress : (record {}) -> (record {}); + get_build_metadata : () -> (text) query; + get_latest_reward_event : () -> (RewardEvent) query; + get_maturity_modulation : (record {}) -> (GetMaturityModulationResponse); + get_metadata : (record {}) -> (GetMetadataResponse) query; + get_mode : (record {}) -> (GetModeResponse) query; + get_nervous_system_parameters : (null) -> (NervousSystemParameters) query; + get_neuron : (GetNeuron) -> (GetNeuronResponse) query; + get_proposal : (GetProposal) -> (GetProposalResponse) query; + get_root_canister_status : (null) -> (CanisterStatusResultV2); + get_running_sns_version : (record {}) -> (GetRunningSnsVersionResponse) query; + get_sns_initialization_parameters : (record {}) -> ( + GetSnsInitializationParametersResponse, + ) query; + list_nervous_system_functions : () -> ( + ListNervousSystemFunctionsResponse, + ) query; + list_neurons : (ListNeurons) -> (ListNeuronsResponse) query; + list_proposals : (ListProposals) -> (ListProposalsResponse) query; + manage_neuron : (ManageNeuron) -> (ManageNeuronResponse); + set_mode : (SetMode) -> (record {}); +} \ No newline at end of file diff --git a/ssr/did/sns_ledger.did b/ssr/did/sns_ledger.did new file mode 100644 index 00000000..e2799db0 --- /dev/null +++ b/ssr/did/sns_ledger.did @@ -0,0 +1,453 @@ +type BlockIndex = nat; +type Subaccount = blob; +// Number of nanoseconds since the UNIX epoch in UTC timezone. +type Timestamp = nat64; +// Number of nanoseconds between two [Timestamp]s. +type Duration = nat64; +type Tokens = nat; +type TxIndex = nat; +type Allowance = record { allowance : nat; expires_at : opt Timestamp }; +type AllowanceArgs = record { account : Account; spender : Account }; +type Approve = record { + fee : opt nat; + from : Account; + memo : opt blob; + created_at_time : opt Timestamp; + amount : nat; + expected_allowance : opt nat; + expires_at : opt Timestamp; + spender : Account; +}; +type ApproveArgs = record { + fee : opt nat; + memo : opt blob; + from_subaccount : opt blob; + created_at_time : opt Timestamp; + amount : nat; + expected_allowance : opt nat; + expires_at : opt Timestamp; + spender : Account; +}; +type ApproveError = variant { + GenericError : record { message : text; error_code : nat }; + TemporarilyUnavailable; + Duplicate : record { duplicate_of : BlockIndex }; + BadFee : record { expected_fee : nat }; + AllowanceChanged : record { current_allowance : nat }; + CreatedInFuture : record { ledger_time : Timestamp }; + TooOld; + Expired : record { ledger_time : Timestamp }; + InsufficientFunds : record { balance : nat }; +}; +type ApproveResult = variant { Ok : BlockIndex; Err : ApproveError }; + +type HttpRequest = record { + url : text; + method : text; + body : blob; + headers : vec record { text; text }; +}; +type HttpResponse = record { + body : blob; + headers : vec record { text; text }; + status_code : nat16; +}; + +type Account = record { + owner : principal; + subaccount : opt Subaccount; +}; + +type TransferArg = record { + from_subaccount : opt Subaccount; + to : Account; + amount : Tokens; + fee : opt Tokens; + memo : opt blob; + created_at_time: opt Timestamp; +}; + +type TransferError = variant { + BadFee : record { expected_fee : Tokens }; + BadBurn : record { min_burn_amount : Tokens }; + InsufficientFunds : record { balance : Tokens }; + TooOld; + CreatedInFuture : record { ledger_time : Timestamp }; + TemporarilyUnavailable; + Duplicate : record { duplicate_of : BlockIndex }; + GenericError : record { error_code : nat; message : text }; +}; + +type TransferResult = variant { + Ok : BlockIndex; + Err : TransferError; +}; + +// The value returned from the [icrc1_metadata] endpoint. +type MetadataValue = variant { + Nat : nat; + Int : int; + Text : text; + Blob : blob; +}; + +type FeatureFlags = record { + icrc2 : bool; +}; + +// The initialization parameters of the Ledger +type InitArgs = record { + minting_account : Account; + fee_collector_account : opt Account; + transfer_fee : nat; + decimals : opt nat8; + max_memo_length : opt nat16; + token_symbol : text; + token_name : text; + metadata : vec record { text; MetadataValue }; + initial_balances : vec record { Account; nat }; + feature_flags : opt FeatureFlags; + maximum_number_of_accounts : opt nat64; + accounts_overflow_trim_quantity : opt nat64; + archive_options : record { + num_blocks_to_archive : nat64; + max_transactions_per_response : opt nat64; + trigger_threshold : nat64; + max_message_size_bytes : opt nat64; + cycles_for_archive_creation : opt nat64; + node_max_memory_size_bytes : opt nat64; + controller_id : principal; + more_controller_ids : opt vec principal; + }; +}; + +type ChangeFeeCollector = variant { + Unset; SetTo: Account; +}; + +type ChangeArchiveOptions = record { + num_blocks_to_archive : opt nat64; + max_transactions_per_response : opt nat64; + trigger_threshold : opt nat64; + max_message_size_bytes : opt nat64; + cycles_for_archive_creation : opt nat64; + node_max_memory_size_bytes : opt nat64; + controller_id : opt principal; + more_controller_ids : opt vec principal; +}; + +type UpgradeArgs = record { + metadata : opt vec record { text; MetadataValue }; + token_symbol : opt text; + token_name : opt text; + transfer_fee : opt nat; + change_fee_collector : opt ChangeFeeCollector; + max_memo_length : opt nat16; + feature_flags : opt FeatureFlags; + maximum_number_of_accounts: opt nat64; + accounts_overflow_trim_quantity: opt nat64; + change_archive_options : opt ChangeArchiveOptions; +}; + +type LedgerArg = variant { + Init: InitArgs; + Upgrade: opt UpgradeArgs; +}; + +type GetTransactionsRequest = record { + // The index of the first tx to fetch. + start : TxIndex; + // The number of transactions to fetch. + length : nat; +}; + +type GetTransactionsResponse = record { + // The total number of transactions in the log. + log_length : nat; + + // List of transaction that were available in the ledger when it processed the call. + // + // The transactions form a contiguous range, with the first transaction having index + // [first_index] (see below), and the last transaction having index + // [first_index] + len(transactions) - 1. + // + // The transaction range can be an arbitrary sub-range of the originally requested range. + transactions : vec Transaction; + + // The index of the first transaction in [transactions]. + // If the transaction vector is empty, the exact value of this field is not specified. + first_index : TxIndex; + + // Encoding of instructions for fetching archived transactions whose indices fall into the + // requested range. + // + // For each entry `e` in [archived_transactions], `[e.from, e.from + len)` is a sub-range + // of the originally requested transaction range. + archived_transactions : vec record { + // The index of the first archived transaction you can fetch using the [callback]. + start : TxIndex; + + // The number of transactions you can fetch using the callback. + length : nat; + + // The function you should call to fetch the archived transactions. + // The range of the transaction accessible using this function is given by [from] + // and [len] fields above. + callback : QueryArchiveFn; + }; +}; + + +// A prefix of the transaction range specified in the [GetTransactionsRequest] request. +type TransactionRange = record { + // A prefix of the requested transaction range. + // The index of the first transaction is equal to [GetTransactionsRequest.from]. + // + // Note that the number of transactions might be less than the requested + // [GetTransactionsRequest.length] for various reasons, for example: + // + // 1. The query might have hit the replica with an outdated state + // that doesn't have the whole range yet. + // 2. The requested range is too large to fit into a single reply. + // + // NOTE: the list of transactions can be empty if: + // + // 1. [GetTransactionsRequest.length] was zero. + // 2. [GetTransactionsRequest.from] was larger than the last transaction known to + // the canister. + transactions : vec Transaction; +}; + +// A function for fetching archived transaction. +type QueryArchiveFn = func (GetTransactionsRequest) -> (TransactionRange) query; + +type Transaction = record { + burn : opt Burn; + kind : text; + mint : opt Mint; + approve : opt Approve; + timestamp : Timestamp; + transfer : opt Transfer; +}; + +type Burn = record { + from : Account; + memo : opt blob; + created_at_time : opt Timestamp; + amount : nat; + spender : opt Account; +}; + +type Mint = record { + to : Account; + memo : opt blob; + created_at_time : opt Timestamp; + amount : nat; +}; + +type Transfer = record { + to : Account; + fee : opt nat; + from : Account; + memo : opt blob; + created_at_time : opt Timestamp; + amount : nat; + spender : opt Account; +}; + +type Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Nat64: nat64; + Int : int; + Array : vec Value; + Map : Map; +}; + +type Map = vec record { text; Value }; + +type Block = Value; + +type GetBlocksArgs = record { + // The index of the first block to fetch. + start : BlockIndex; + // Max number of blocks to fetch. + length : nat; +}; + +// A prefix of the block range specified in the [GetBlocksArgs] request. +type BlockRange = record { + // A prefix of the requested block range. + // The index of the first block is equal to [GetBlocksArgs.start]. + // + // Note that the number of blocks might be less than the requested + // [GetBlocksArgs.length] for various reasons, for example: + // + // 1. The query might have hit the replica with an outdated state + // that doesn't have the whole range yet. + // 2. The requested range is too large to fit into a single reply. + // + // NOTE: the list of blocks can be empty if: + // + // 1. [GetBlocksArgs.length] was zero. + // 2. [GetBlocksArgs.start] was larger than the last block known to + // the canister. + blocks : vec Block; +}; + +// A function for fetching archived blocks. +type QueryBlockArchiveFn = func (GetBlocksArgs) -> (BlockRange) query; + +// The result of a "get_blocks" call. +type GetBlocksResponse = record { + // The index of the first block in "blocks". + // If the blocks vector is empty, the exact value of this field is not specified. + first_index : BlockIndex; + + // The total number of blocks in the chain. + // If the chain length is positive, the index of the last block is `chain_len - 1`. + chain_length : nat64; + + // System certificate for the hash of the latest block in the chain. + // Only present if `get_blocks` is called in a non-replicated query context. + certificate : opt blob; + + // List of blocks that were available in the ledger when it processed the call. + // + // The blocks form a contiguous range, with the first block having index + // [first_block_index] (see below), and the last block having index + // [first_block_index] + len(blocks) - 1. + // + // The block range can be an arbitrary sub-range of the originally requested range. + blocks : vec Block; + + // Encoding of instructions for fetching archived blocks. + archived_blocks : vec record { + // The index of the first archived block. + start : BlockIndex; + + // The number of blocks that can be fetched. + length : nat; + + // Callback to fetch the archived blocks. + callback : QueryBlockArchiveFn; + }; +}; + +// Certificate for the block at `block_index`. +type DataCertificate = record { + certificate : opt blob; + hash_tree : blob; +}; + +type StandardRecord = record { url : text; name : text }; + +type TransferFromArgs = record { + spender_subaccount : opt Subaccount; + from : Account; + to : Account; + amount : Tokens; + fee : opt Tokens; + memo : opt blob; + created_at_time: opt Timestamp; +}; + +type TransferFromResult = variant { + Ok : BlockIndex; + Err : TransferFromError; +}; + +type TransferFromError = variant { + BadFee : record { expected_fee : Tokens }; + BadBurn : record { min_burn_amount : Tokens }; + InsufficientFunds : record { balance : Tokens }; + InsufficientAllowance : record { allowance : Tokens }; + TooOld; + CreatedInFuture : record { ledger_time : Timestamp }; + Duplicate : record { duplicate_of : BlockIndex }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type ArchiveInfo = record { + canister_id: principal; + block_range_start: BlockIndex; + block_range_end: BlockIndex; +}; + +type ICRC3Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Int : int; + Array : vec ICRC3Value; + Map : vec record { text; ICRC3Value }; +}; + +type GetArchivesArgs = record { + // The last archive seen by the client. + // The Ledger will return archives coming + // after this one if set, otherwise it + // will return the first archives. + from : opt principal; +}; + +type GetArchivesResult = vec record { + // The id of the archive + canister_id : principal; + + // The first block in the archive + start : nat; + + // The last block in the archive + end : nat; +}; + +type GetBlocksResult = record { + // Total number of blocks in the + // block log + log_length : nat; + + blocks : vec record { id : nat; block: ICRC3Value }; + + archived_blocks : vec record { + args : vec GetBlocksArgs; + callback : func (vec GetBlocksArgs) -> (GetBlocksResult) query; + }; +}; + +type ICRC3DataCertificate = record { + // See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification + certificate : blob; + + // CBOR encoded hash_tree + hash_tree : blob; +}; + +service : (ledger_arg : LedgerArg) -> { + archives : () -> (vec ArchiveInfo) query; + get_transactions : (GetTransactionsRequest) -> (GetTransactionsResponse) query; + get_blocks : (GetBlocksArgs) -> (GetBlocksResponse) query; + get_data_certificate : () -> (DataCertificate) query; + + icrc1_name : () -> (text) query; + icrc1_symbol : () -> (text) query; + icrc1_decimals : () -> (nat8) query; + icrc1_metadata : () -> (vec record { text; MetadataValue }) query; + icrc1_total_supply : () -> (Tokens) query; + icrc1_fee : () -> (Tokens) query; + icrc1_minting_account : () -> (opt Account) query; + icrc1_balance_of : (Account) -> (Tokens) query; + icrc1_transfer : (TransferArg) -> (TransferResult); + icrc1_supported_standards : () -> (vec StandardRecord) query; + + icrc2_approve : (ApproveArgs) -> (ApproveResult); + icrc2_allowance : (AllowanceArgs) -> (Allowance) query; + icrc2_transfer_from : (TransferFromArgs) -> (TransferFromResult); + + icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; + icrc3_get_tip_certificate : () -> (opt ICRC3DataCertificate) query; + icrc3_get_blocks : (vec GetBlocksArgs) -> (GetBlocksResult) query; + icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query; +} diff --git a/ssr/did/sns_root.did b/ssr/did/sns_root.did new file mode 100644 index 00000000..da738d68 --- /dev/null +++ b/ssr/did/sns_root.did @@ -0,0 +1,113 @@ +type CanisterCallError = record { code : opt int32; description : text }; +type CanisterIdRecord = record { canister_id : principal }; +type CanisterInstallMode = variant { reinstall; upgrade; install }; +type CanisterStatusResult = record { + status : CanisterStatusType; + memory_size : nat; + cycles : nat; + settings : DefiniteCanisterSettings; + idle_cycles_burned_per_day : opt nat; + module_hash : opt blob; + reserved_cycles : opt nat; +}; +type CanisterStatusResultV2 = record { + status : CanisterStatusType; + memory_size : nat; + cycles : nat; + settings : DefiniteCanisterSettingsArgs; + idle_cycles_burned_per_day : nat; + module_hash : opt blob; +}; +type CanisterStatusType = variant { stopped; stopping; running }; +type CanisterSummary = record { + status : opt CanisterStatusResultV2; + canister_id : opt principal; +}; +type ChangeCanisterRequest = record { + arg : blob; + wasm_module : blob; + stop_before_installing : bool; + mode : CanisterInstallMode; + canister_id : principal; + memory_allocation : opt nat; + compute_allocation : opt nat; +}; +type DefiniteCanisterSettings = record { + freezing_threshold : opt nat; + controllers : vec principal; + reserved_cycles_limit : opt nat; + memory_allocation : opt nat; + compute_allocation : opt nat; +}; +type DefiniteCanisterSettingsArgs = record { + freezing_threshold : nat; + controllers : vec principal; + memory_allocation : nat; + compute_allocation : nat; +}; +type FailedUpdate = record { + err : opt CanisterCallError; + dapp_canister_id : opt principal; +}; +type GetSnsCanistersSummaryRequest = record { update_canister_list : opt bool }; +type GetSnsCanistersSummaryResponse = record { + root : opt CanisterSummary; + swap : opt CanisterSummary; + ledger : opt CanisterSummary; + index : opt CanisterSummary; + governance : opt CanisterSummary; + dapps : vec CanisterSummary; + archives : vec CanisterSummary; +}; +type ListSnsCanistersResponse = record { + root : opt principal; + swap : opt principal; + ledger : opt principal; + index : opt principal; + governance : opt principal; + dapps : vec principal; + archives : vec principal; +}; +type ManageDappCanisterSettingsRequest = record { + freezing_threshold : opt nat64; + canister_ids : vec principal; + reserved_cycles_limit : opt nat64; + log_visibility : opt int32; + memory_allocation : opt nat64; + compute_allocation : opt nat64; +}; +type ManageDappCanisterSettingsResponse = record { failure_reason : opt text }; +type RegisterDappCanisterRequest = record { canister_id : opt principal }; +type RegisterDappCanistersRequest = record { canister_ids : vec principal }; +type SetDappControllersRequest = record { + canister_ids : opt RegisterDappCanistersRequest; + controller_principal_ids : vec principal; +}; +type SetDappControllersResponse = record { failed_updates : vec FailedUpdate }; +type SnsRootCanister = record { + dapp_canister_ids : vec principal; + testflight : bool; + latest_ledger_archive_poll_timestamp_seconds : opt nat64; + archive_canister_ids : vec principal; + governance_canister_id : opt principal; + index_canister_id : opt principal; + swap_canister_id : opt principal; + ledger_canister_id : opt principal; +}; +service : (SnsRootCanister) -> { + canister_status : (CanisterIdRecord) -> (CanisterStatusResult); + change_canister : (ChangeCanisterRequest) -> (); + get_build_metadata : () -> (text) query; + get_sns_canisters_summary : (GetSnsCanistersSummaryRequest) -> ( + GetSnsCanistersSummaryResponse, + ); + list_sns_canisters : (record {}) -> (ListSnsCanistersResponse) query; + manage_dapp_canister_settings : (ManageDappCanisterSettingsRequest) -> ( + ManageDappCanisterSettingsResponse, + ); + register_dapp_canister : (RegisterDappCanisterRequest) -> (record {}); + register_dapp_canisters : (RegisterDappCanistersRequest) -> (record {}); + set_dapp_controllers : (SetDappControllersRequest) -> ( + SetDappControllersResponse, + ); +} \ No newline at end of file diff --git a/ssr/did/sns_swap.did b/ssr/did/sns_swap.did new file mode 100644 index 00000000..2a24c418 --- /dev/null +++ b/ssr/did/sns_swap.did @@ -0,0 +1,314 @@ +type BuyerState = record { + icp : opt TransferableAmount; + has_created_neuron_recipes : opt bool; +}; +type CanisterCallError = record { code : opt int32; description : text }; +type CanisterStatusResultV2 = record { + status : CanisterStatusType; + memory_size : nat; + cycles : nat; + settings : DefiniteCanisterSettingsArgs; + idle_cycles_burned_per_day : nat; + module_hash : opt blob; +}; +type CanisterStatusType = variant { stopped; stopping; running }; +type CfInvestment = record { + controller : opt principal; + hotkey_principal : text; + hotkeys : opt Principals; + nns_neuron_id : nat64; +}; +type CfNeuron = record { + has_created_neuron_recipes : opt bool; + hotkeys : opt Principals; + nns_neuron_id : nat64; + amount_icp_e8s : nat64; +}; +type CfParticipant = record { + controller : opt principal; + hotkey_principal : text; + cf_neurons : vec CfNeuron; +}; +type Countries = record { iso_codes : vec text }; +type DefiniteCanisterSettingsArgs = record { + freezing_threshold : nat; + controllers : vec principal; + wasm_memory_limit : opt nat; + memory_allocation : nat; + compute_allocation : nat; +}; +type DerivedState = record { + sns_tokens_per_icp : float32; + buyer_total_icp_e8s : nat64; + cf_participant_count : opt nat64; + neurons_fund_participation_icp_e8s : opt nat64; + direct_participation_icp_e8s : opt nat64; + direct_participant_count : opt nat64; + cf_neuron_count : opt nat64; +}; +type DirectInvestment = record { buyer_principal : text }; +type Err = record { description : opt text; error_type : opt int32 }; +type Err_1 = record { error_type : opt int32 }; +type Err_2 = record { + invalid_user_amount : opt InvalidUserAmount; + existing_ticket : opt Ticket; + error_type : int32; +}; +type Error = record { message : opt text }; +type ErrorRefundIcpRequest = record { source_principal_id : opt principal }; +type ErrorRefundIcpResponse = record { result : opt Result }; +type FailedUpdate = record { + err : opt CanisterCallError; + dapp_canister_id : opt principal; +}; +type FinalizeSwapResponse = record { + set_dapp_controllers_call_result : opt SetDappControllersCallResult; + create_sns_neuron_recipes_result : opt SweepResult; + settle_community_fund_participation_result : opt SettleCommunityFundParticipationResult; + error_message : opt text; + settle_neurons_fund_participation_result : opt SettleNeuronsFundParticipationResult; + set_mode_call_result : opt SetModeCallResult; + sweep_icp_result : opt SweepResult; + claim_neuron_result : opt SweepResult; + sweep_sns_result : opt SweepResult; +}; +type GetAutoFinalizationStatusResponse = record { + auto_finalize_swap_response : opt FinalizeSwapResponse; + has_auto_finalize_been_attempted : opt bool; + is_auto_finalize_enabled : opt bool; +}; +type GetBuyerStateRequest = record { principal_id : opt principal }; +type GetBuyerStateResponse = record { buyer_state : opt BuyerState }; +type GetBuyersTotalResponse = record { buyers_total : nat64 }; +type GetDerivedStateResponse = record { + sns_tokens_per_icp : opt float64; + buyer_total_icp_e8s : opt nat64; + cf_participant_count : opt nat64; + neurons_fund_participation_icp_e8s : opt nat64; + direct_participation_icp_e8s : opt nat64; + direct_participant_count : opt nat64; + cf_neuron_count : opt nat64; +}; +type GetInitResponse = record { init : opt Init }; +type GetLifecycleResponse = record { + decentralization_sale_open_timestamp_seconds : opt nat64; + lifecycle : opt int32; + decentralization_swap_termination_timestamp_seconds : opt nat64; +}; +type GetOpenTicketResponse = record { result : opt Result_1 }; +type GetSaleParametersResponse = record { params : opt Params }; +type GetStateResponse = record { swap : opt Swap; derived : opt DerivedState }; +type GovernanceError = record { error_message : text; error_type : int32 }; +type Icrc1Account = record { owner : opt principal; subaccount : opt blob }; +type IdealMatchedParticipationFunction = record { + serialized_representation : opt text; +}; +type Init = record { + nns_proposal_id : opt nat64; + sns_root_canister_id : text; + neurons_fund_participation : opt bool; + min_participant_icp_e8s : opt nat64; + neuron_basket_construction_parameters : opt NeuronBasketConstructionParameters; + fallback_controller_principal_ids : vec text; + max_icp_e8s : opt nat64; + neuron_minimum_stake_e8s : opt nat64; + confirmation_text : opt text; + swap_start_timestamp_seconds : opt nat64; + swap_due_timestamp_seconds : opt nat64; + min_participants : opt nat32; + sns_token_e8s : opt nat64; + nns_governance_canister_id : text; + transaction_fee_e8s : opt nat64; + icp_ledger_canister_id : text; + sns_ledger_canister_id : text; + neurons_fund_participation_constraints : opt NeuronsFundParticipationConstraints; + should_auto_finalize : opt bool; + max_participant_icp_e8s : opt nat64; + sns_governance_canister_id : text; + min_direct_participation_icp_e8s : opt nat64; + restricted_countries : opt Countries; + min_icp_e8s : opt nat64; + max_direct_participation_icp_e8s : opt nat64; +}; +type InvalidUserAmount = record { + min_amount_icp_e8s_included : nat64; + max_amount_icp_e8s_included : nat64; +}; +type Investor = variant { + CommunityFund : CfInvestment; + Direct : DirectInvestment; +}; +type LinearScalingCoefficient = record { + slope_numerator : opt nat64; + intercept_icp_e8s : opt nat64; + from_direct_participation_icp_e8s : opt nat64; + slope_denominator : opt nat64; + to_direct_participation_icp_e8s : opt nat64; +}; +type ListCommunityFundParticipantsRequest = record { + offset : opt nat64; + limit : opt nat32; +}; +type ListCommunityFundParticipantsResponse = record { + cf_participants : vec CfParticipant; +}; +type ListDirectParticipantsRequest = record { + offset : opt nat32; + limit : opt nat32; +}; +type ListDirectParticipantsResponse = record { participants : vec Participant }; +type ListSnsNeuronRecipesRequest = record { + offset : opt nat64; + limit : opt nat32; +}; +type ListSnsNeuronRecipesResponse = record { + sns_neuron_recipes : vec SnsNeuronRecipe; +}; +type NeuronAttributes = record { + dissolve_delay_seconds : nat64; + memo : nat64; + followees : vec NeuronId; +}; +type NeuronBasketConstructionParameters = record { + dissolve_delay_interval_seconds : nat64; + count : nat64; +}; +type NeuronId = record { id : blob }; +type NeuronsFundParticipationConstraints = record { + coefficient_intervals : vec LinearScalingCoefficient; + max_neurons_fund_participation_icp_e8s : opt nat64; + min_direct_participation_threshold_icp_e8s : opt nat64; + ideal_matched_participation_function : opt IdealMatchedParticipationFunction; +}; +type NewSaleTicketRequest = record { + subaccount : opt blob; + amount_icp_e8s : nat64; +}; +type NewSaleTicketResponse = record { result : opt Result_2 }; +type Ok = record { block_height : opt nat64 }; +type Ok_1 = record { + neurons_fund_participation_icp_e8s : opt nat64; + neurons_fund_neurons_count : opt nat64; +}; +type Ok_2 = record { ticket : opt Ticket }; +type Params = record { + min_participant_icp_e8s : nat64; + neuron_basket_construction_parameters : opt NeuronBasketConstructionParameters; + max_icp_e8s : nat64; + swap_due_timestamp_seconds : nat64; + min_participants : nat32; + sns_token_e8s : nat64; + sale_delay_seconds : opt nat64; + max_participant_icp_e8s : nat64; + min_direct_participation_icp_e8s : opt nat64; + min_icp_e8s : nat64; + max_direct_participation_icp_e8s : opt nat64; +}; +type Participant = record { + participation : opt BuyerState; + participant_id : opt principal; +}; +type Possibility = variant { + Ok : SetDappControllersResponse; + Err : CanisterCallError; +}; +type Possibility_1 = variant { Ok : Response; Err : CanisterCallError }; +type Possibility_2 = variant { Ok : Ok_1; Err : Error }; +type Possibility_3 = variant { Ok : record {}; Err : CanisterCallError }; +type Principals = record { principals : vec principal }; +type RefreshBuyerTokensRequest = record { + confirmation_text : opt text; + buyer : text; +}; +type RefreshBuyerTokensResponse = record { + icp_accepted_participation_e8s : nat64; + icp_ledger_account_balance_e8s : nat64; +}; +type Response = record { governance_error : opt GovernanceError }; +type Result = variant { Ok : Ok; Err : Err }; +type Result_1 = variant { Ok : Ok_2; Err : Err_1 }; +type Result_2 = variant { Ok : Ok_2; Err : Err_2 }; +type SetDappControllersCallResult = record { possibility : opt Possibility }; +type SetDappControllersResponse = record { failed_updates : vec FailedUpdate }; +type SetModeCallResult = record { possibility : opt Possibility_3 }; +type SettleCommunityFundParticipationResult = record { + possibility : opt Possibility_1; +}; +type SettleNeuronsFundParticipationResult = record { + possibility : opt Possibility_2; +}; +type SnsNeuronRecipe = record { + sns : opt TransferableAmount; + claimed_status : opt int32; + neuron_attributes : opt NeuronAttributes; + investor : opt Investor; +}; +type Swap = record { + auto_finalize_swap_response : opt FinalizeSwapResponse; + neuron_recipes : vec SnsNeuronRecipe; + next_ticket_id : opt nat64; + decentralization_sale_open_timestamp_seconds : opt nat64; + finalize_swap_in_progress : opt bool; + cf_participants : vec CfParticipant; + init : opt Init; + already_tried_to_auto_finalize : opt bool; + neurons_fund_participation_icp_e8s : opt nat64; + purge_old_tickets_last_completion_timestamp_nanoseconds : opt nat64; + direct_participation_icp_e8s : opt nat64; + lifecycle : int32; + purge_old_tickets_next_principal : opt blob; + decentralization_swap_termination_timestamp_seconds : opt nat64; + buyers : vec record { text; BuyerState }; + params : opt Params; + open_sns_token_swap_proposal_id : opt nat64; +}; +type SweepResult = record { + failure : nat32; + skipped : nat32; + invalid : nat32; + success : nat32; + global_failures : nat32; +}; +type Ticket = record { + creation_time : nat64; + ticket_id : nat64; + account : opt Icrc1Account; + amount_icp_e8s : nat64; +}; +type TransferableAmount = record { + transfer_fee_paid_e8s : opt nat64; + transfer_start_timestamp_seconds : nat64; + amount_e8s : nat64; + amount_transferred_e8s : opt nat64; + transfer_success_timestamp_seconds : nat64; +}; +service : (Init) -> { + error_refund_icp : (ErrorRefundIcpRequest) -> (ErrorRefundIcpResponse); + finalize_swap : (record {}) -> (FinalizeSwapResponse); + get_auto_finalization_status : (record {}) -> ( + GetAutoFinalizationStatusResponse, + ) query; + get_buyer_state : (GetBuyerStateRequest) -> (GetBuyerStateResponse) query; + get_buyers_total : (record {}) -> (GetBuyersTotalResponse); + get_canister_status : (record {}) -> (CanisterStatusResultV2); + get_derived_state : (record {}) -> (GetDerivedStateResponse) query; + get_init : (record {}) -> (GetInitResponse) query; + get_lifecycle : (record {}) -> (GetLifecycleResponse) query; + get_open_ticket : (record {}) -> (GetOpenTicketResponse) query; + get_sale_parameters : (record {}) -> (GetSaleParametersResponse) query; + get_state : (record {}) -> (GetStateResponse) query; + list_community_fund_participants : (ListCommunityFundParticipantsRequest) -> ( + ListCommunityFundParticipantsResponse, + ) query; + list_direct_participants : (ListDirectParticipantsRequest) -> ( + ListDirectParticipantsResponse, + ) query; + list_sns_neuron_recipes : (ListSnsNeuronRecipesRequest) -> ( + ListSnsNeuronRecipesResponse, + ) query; + new_sale_ticket : (NewSaleTicketRequest) -> (NewSaleTicketResponse); + notify_payment_failure : (record {}) -> (Ok_2); + refresh_buyer_tokens : (RefreshBuyerTokensRequest) -> ( + RefreshBuyerTokensResponse, + ); +} \ No newline at end of file diff --git a/ssr/did/user_index.did b/ssr/did/user_index.did index f20d3e9b..1987e89a 100644 --- a/ssr/did/user_index.did +++ b/ssr/did/user_index.did @@ -1,4 +1,17 @@ -type CanisterInstallMode = variant { reinstall; upgrade; install }; +type BroadcastCallStatus = record { + successful_canister_ids : vec principal; + failed_canisters_count : nat64; + successful_canisters_count : nat64; + method_name : text; + failed_canister_ids : vec record { principal; text }; + timestamp : SystemTime; + total_canisters : nat64; +}; +type CanisterInstallMode = variant { + reinstall; + upgrade : opt opt bool; + install; +}; type CanisterStatusResponse = record { status : CanisterStatusType; memory_size : nat; @@ -6,23 +19,27 @@ type CanisterStatusResponse = record { settings : DefiniteCanisterSettings; query_stats : QueryStats; idle_cycles_burned_per_day : nat; - module_hash : opt vec nat8; + module_hash : opt blob; + reserved_cycles : nat; }; type CanisterStatusType = variant { stopped; stopping; running }; type DefiniteCanisterSettings = record { freezing_threshold : nat; controllers : vec principal; + reserved_cycles_limit : nat; + log_visibility : LogVisibility; + wasm_memory_limit : nat; memory_allocation : nat; compute_allocation : nat; }; type HttpRequest = record { url : text; method : text; - body : vec nat8; + body : blob; headers : vec record { text; text }; }; type HttpResponse = record { - body : vec nat8; + body : blob; headers : vec record { text; text }; status_code : nat16; }; @@ -30,21 +47,31 @@ type KnownPrincipalType = variant { CanisterIdUserIndex; CanisterIdPlatformOrchestrator; CanisterIdConfiguration; + CanisterIdHotOrNotSubnetOrchestrator; CanisterIdProjectMemberIndex; CanisterIdTopicCacheIndex; CanisterIdRootCanister; CanisterIdDataBackup; + CanisterIdSnsWasm; CanisterIdPostCache; CanisterIdSNSController; CanisterIdSnsGovernance; UserIdGlobalSuperAdmin; }; +type LogVisibility = variant { controllers; public }; type QueryStats = record { response_payload_bytes_total : nat; num_instructions_total : nat; num_calls_total : nat; request_payload_bytes_total : nat; }; +type RecycleStatus = record { + last_recycled_duration : opt nat64; + last_recycled_at : opt SystemTime; + num_last_recycled_canisters : nat64; + success_canisters : vec text; + failed_recycling : vec record { principal; text }; +}; type RejectionCode = variant { NoError; CanisterError; @@ -55,12 +82,13 @@ type RejectionCode = variant { CanisterReject; }; type Result = variant { Ok : text; Err : text }; -type Result_1 = variant { +type Result_1 = variant { Ok : principal; Err : text }; +type Result_2 = variant { Ok : record { CanisterStatusResponse }; Err : record { RejectionCode; text }; }; -type Result_2 = variant { Ok; Err : text }; -type Result_3 = variant { Ok; Err : SetUniqueUsernameError }; +type Result_3 = variant { Ok; Err : text }; +type Result_4 = variant { Ok; Err : SetUniqueUsernameError }; type SetUniqueUsernameError = variant { UsernameAlreadyTaken; SendingCanisterDoesNotMatchUserCanisterId; @@ -90,16 +118,16 @@ type UserIndexInitArgs = record { }; service : (UserIndexInitArgs) -> { are_signups_enabled : () -> (bool) query; - backup_all_individual_user_canisters : () -> (); - create_pool_of_individual_user_available_canisters : (text, vec nat8) -> ( - Result, - ); + create_pool_of_individual_user_available_canisters : (text, blob) -> (Result); get_current_list_of_all_well_known_principal_values : () -> ( vec record { KnownPrincipalType; principal }, ) query; get_index_details_is_user_name_taken : (text) -> (bool) query; get_index_details_last_upgrade_status : () -> (UpgradeStatus) query; + get_last_broadcast_call_status : () -> (BroadcastCallStatus) query; get_list_of_available_canisters : () -> (vec principal) query; + get_recycle_status : () -> (RecycleStatus) query; + get_requester_principals_canister_id_create_if_not_exists : () -> (Result_1); get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer : () -> ( principal, ); @@ -111,7 +139,10 @@ service : (UserIndexInitArgs) -> { ) query; get_user_canister_incl_avail_list : () -> (vec principal) query; get_user_canister_list : () -> (vec principal) query; - get_user_canister_status : (principal) -> (Result_1); + get_user_canister_status : (principal) -> (Result_2); + get_user_id_and_canister_list : () -> ( + vec record { principal; principal }, + ) query; get_user_index_canister_count : () -> (nat64) query; get_user_index_canister_cycle_balance : () -> (nat) query; get_well_known_principal_value : (KnownPrincipalType) -> ( @@ -119,22 +150,21 @@ service : (UserIndexInitArgs) -> { ) query; http_request : (HttpRequest) -> (HttpResponse) query; issue_rewards_for_referral : (principal, principal, principal) -> (Result); - receive_data_from_backup_canister_and_restore_data_to_heap : ( - principal, - principal, - text, - ) -> (); reclaim_cycles_from_individual_canisters : () -> (); + request_cycles : (nat) -> (Result_3); reset_user_individual_canisters : (vec principal) -> (Result); return_cycles_to_platform_orchestrator_canister : () -> (Result); set_permission_to_upgrade_individual_canisters : (bool) -> (text); - start_upgrades_for_individual_canisters : (text, vec nat8) -> (text); - toggle_signups_enabled : () -> (Result_2); + start_upgrades_for_individual_canisters : (text, blob) -> (text); + toggle_signups_enabled : () -> (Result_3); + update_canisters_last_functionality_access_time : () -> (text); update_index_with_unique_user_name_corresponding_to_user_principal_id : ( text, principal, - ) -> (Result_3); + ) -> (Result_4); update_profile_owner_for_individual_canisters : () -> (); + update_restart_timers_hon_game : () -> (text); + update_well_known_principal : (KnownPrincipalType, principal) -> (); upgrade_specific_individual_user_canister_with_latest_wasm : ( principal, opt principal, diff --git a/ssr/public/firebase-messaging-sw.js b/ssr/public/firebase-messaging-sw.js index 6f9375f1..baa4da40 100644 --- a/ssr/public/firebase-messaging-sw.js +++ b/ssr/public/firebase-messaging-sw.js @@ -1,5 +1,9 @@ -importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js'); -importScripts('https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js'); +importScripts( + "https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js", +); +importScripts( + "https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js", +); firebase.initializeApp({ // https://firebase.google.com/docs/projects/api-keys#:~:text=it%27s%20OK%20to%20include%20Firebase%20API%20keys%20in%20your%20code @@ -8,7 +12,7 @@ firebase.initializeApp({ projectId: "hot-or-not-feed-intelligence", storageBucket: "hot-or-not-feed-intelligence.appspot.com", messagingSenderId: "82502260393", - appId: "1:82502260393:web:390e9d4e588cba65237bb8" + appId: "1:82502260393:web:390e9d4e588cba65237bb8", }); -const messaging = firebase.messaging(); \ No newline at end of file +const messaging = firebase.messaging(); diff --git a/ssr/public/img/decorator/buy_coin.svg b/ssr/public/img/decorator/buy_coin.svg new file mode 100644 index 00000000..9185de26 --- /dev/null +++ b/ssr/public/img/decorator/buy_coin.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/public/img/decorator/coin_arrow.svg b/ssr/public/img/decorator/coin_arrow.svg new file mode 100644 index 00000000..e3241c53 --- /dev/null +++ b/ssr/public/img/decorator/coin_arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ssr/public/img/decorator/decore-left.svg b/ssr/public/img/decorator/decore-left.svg new file mode 100644 index 00000000..95c395f2 --- /dev/null +++ b/ssr/public/img/decorator/decore-left.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ssr/public/img/decorator/decore-right.svg b/ssr/public/img/decorator/decore-right.svg new file mode 100644 index 00000000..6e28c8a9 --- /dev/null +++ b/ssr/public/img/decorator/decore-right.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ssr/public/img/decorator/hot_arrow.svg b/ssr/public/img/decorator/hot_arrow.svg new file mode 100644 index 00000000..042cdb73 --- /dev/null +++ b/ssr/public/img/decorator/hot_arrow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ssr/public/img/decorator/not_arrow.svg b/ssr/public/img/decorator/not_arrow.svg new file mode 100644 index 00000000..e1c7651e --- /dev/null +++ b/ssr/public/img/decorator/not_arrow.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ssr/public/img/decorator/prizes.svg b/ssr/public/img/decorator/prizes.svg new file mode 100644 index 00000000..dbcfb137 --- /dev/null +++ b/ssr/public/img/decorator/prizes.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/public/img/decorator/star.svg b/ssr/public/img/decorator/star.svg new file mode 100644 index 00000000..0917f246 --- /dev/null +++ b/ssr/public/img/decorator/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/public/img/edit.svg b/ssr/public/img/edit.svg new file mode 100644 index 00000000..db923807 --- /dev/null +++ b/ssr/public/img/edit.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/public/img/info.svg b/ssr/public/img/info.svg new file mode 100644 index 00000000..a512a37c --- /dev/null +++ b/ssr/public/img/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/public/img/upload.svg b/ssr/public/img/upload.svg new file mode 100644 index 00000000..1af1bf80 --- /dev/null +++ b/ssr/public/img/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/ssr/src/app.rs b/ssr/src/app.rs index 687e5a3d..cd275b22 100644 --- a/ssr/src/app.rs +++ b/ssr/src/app.rs @@ -1,3 +1,5 @@ +use crate::page::icpump::ICPumpLanding; +use crate::page::profile::YourProfileView; use crate::{ component::{base_route::BaseRoute, nav::NavBar}, error_template::{AppError, ErrorTemplate}, @@ -14,8 +16,14 @@ use crate::{ root::RootPage, settings::Settings, terms::TermsOfService, + token::{ + create::{CreateToken, CreateTokenCtx, CreateTokenSettings}, + create_token_faq::CreateTokenFAQ, + info::TokenInfo, + transfer::TokenTransfer, + }, upload::UploadPostPage, - wallet::{transactions::Transactions, Wallet}, + wallet::{tokens::Tokens, transactions::Transactions, Wallet}, }, state::{ audio_state::AudioState, canisters::Canisters, content_seed_client::ContentSeedClient, @@ -23,6 +31,7 @@ use crate::{ }, utils::event_streaming::EventHistory, }; + use leptos::*; use leptos_meta::*; use leptos_router::*; @@ -31,7 +40,7 @@ use leptos_router::*; fn NotFound() -> impl IntoView { let mut outside_errors = Errors::default(); outside_errors.insert_with_default_key(AppError::NotFound); - view! { } + view! { } } #[component(transparent)] @@ -40,11 +49,11 @@ fn GoogleAuthRedirectHandlerRoute() -> impl IntoView { #[cfg(any(feature = "oauth-ssr", feature = "oauth-hydrate"))] { use crate::page::google_redirect::GoogleRedirectHandler; - view! { } + view! { } } #[cfg(not(any(feature = "oauth-ssr", feature = "oauth-hydrate")))] { - view! { } + view! { } } } @@ -54,11 +63,11 @@ fn GoogleAuthRedirectorRoute() -> impl IntoView { #[cfg(any(feature = "oauth-ssr", feature = "oauth-hydrate"))] { use crate::page::google_redirect::GoogleRedirector; - view! { } + view! { } } #[cfg(not(any(feature = "oauth-ssr", feature = "oauth-hydrate")))] { - view! { } + view! { } } } @@ -72,6 +81,7 @@ pub fn App() -> impl IntoView { provide_context(ProfilePostsContext::default()); provide_context(AuthorizedUserToSeedContent::default()); provide_context(AudioState::default()); + provide_context(CreateTokenCtx::default()); #[cfg(feature = "hydrate")] { @@ -92,17 +102,16 @@ pub fn App() -> impl IntoView { #[cfg(feature = "ga4")] { enable_ga4_script.set(true); - provide_context(EventHistory::default()); } view! { - + // sets the document title - + <Title text="Yral"/> - <Link rel="manifest" href="/app.webmanifest" /> + <Link rel="manifest" href="/app.webmanifest"/> // GA4 Global Site Tag (gtag.js) - Google Analytics // G-6W5Q2MRX0E to test locally | G-PLNNETMSLM @@ -128,19 +137,19 @@ pub fn App() -> impl IntoView { // auth redirect routes exist outside main context <GoogleAuthRedirectHandlerRoute/> <GoogleAuthRedirectorRoute/> - <Route path="/" view=RootPage/> <Route path="" view=BaseRoute> + <Route path="/" view=RootPage/> <Route path="/hot-or-not/:canister_id/:post_id" view=PostView/> <Route path="/post/:canister_id/:post_id" view=SinglePost/> <Route path="/profile/:canister_id/:post_id" view=ProfilePost/> <Route path="/your-profile/:canister_id/:post_id" view=ProfilePost/> <Route path="/profile/:id" view=ProfileView/> - <Route path="/your-profile/:id" view=ProfileView/> <Route path="/upload" view=UploadPostPage/> <Route path="/error" view=ServerErrorPage/> <Route path="/menu" view=Menu/> <Route path="/settings" view=Settings/> <Route path="/refer-earn" view=ReferEarn/> + <Route path="/your-profile" view=YourProfileView/> <Route path="/terms-of-service" view=TermsOfService/> <Route path="/privacy-policy" view=PrivacyPolicy/> <Route path="/wallet" view=Wallet/> @@ -148,6 +157,13 @@ pub fn App() -> impl IntoView { <Route path="/leaderboard" view=Leaderboard/> <Route path="/account-transfer" view=AccountTransfer/> <Route path="/logout" view=Logout/> + <Route path="/token/create" view=CreateToken/> + <Route path="/token/create/settings" view=CreateTokenSettings/> + <Route path="/token/create/faq" view=CreateTokenFAQ/> + <Route path="/token/info/:token_root/:user_principal" view=TokenInfo/> + <Route path="/token/transfer/:token_root" view=TokenTransfer/> + <Route path="/tokens" view=Tokens/> + <Route path="/board" view=ICPumpLanding/> </Route> </Routes> diff --git a/ssr/src/auth/mod.rs b/ssr/src/auth/mod.rs index ecef99ab..10011a04 100644 --- a/ssr/src/auth/mod.rs +++ b/ssr/src/auth/mod.rs @@ -12,7 +12,7 @@ use rand_chacha::rand_core::OsRng; use serde::{Deserialize, Serialize}; use web_time::Duration; -use crate::{consts::auth::DELEGATION_MAX_AGE, utils::current_epoch}; +use crate::{consts::auth::DELEGATION_MAX_AGE, utils::time::current_epoch}; /// Delegated identity that can be serialized over the wire #[derive(Serialize, Deserialize, Clone)] @@ -122,12 +122,15 @@ pub mod core_clients { pub struct CoreClients { pub google_oauth: openidconnect::core::CoreClient, pub hotornot_google_oauth: openidconnect::core::CoreClient, + pub icpump_google_oauth: openidconnect::core::CoreClient, } impl CoreClients { pub fn get_oauth_client(&self, host: &str) -> openidconnect::core::CoreClient { if host == "hotornot.wtf" { self.hotornot_google_oauth.clone() + } else if host == "icpump.fun" { + self.icpump_google_oauth.clone() } else { self.google_oauth.clone() } diff --git a/ssr/src/auth/server_impl/mod.rs b/ssr/src/auth/server_impl/mod.rs index 4222aafa..c39c2c68 100644 --- a/ssr/src/auth/server_impl/mod.rs +++ b/ssr/src/auth/server_impl/mod.rs @@ -17,7 +17,7 @@ use rand_chacha::rand_core::OsRng; use crate::{ consts::auth::{REFRESH_MAX_AGE, REFRESH_TOKEN_COOKIE}, - utils::current_epoch, + utils::time::current_epoch, }; use self::store::{KVStore, KVStoreImpl}; @@ -36,7 +36,7 @@ fn set_cookies(resp: &ResponseOptions, jar: impl IntoResponse) { } } -fn extract_principal_from_cookie( +pub fn extract_principal_from_cookie( jar: &SignedCookieJar, ) -> Result<Option<Principal>, ServerFnError> { let Some(cookie) = jar.get(REFRESH_TOKEN_COOKIE) else { diff --git a/ssr/src/component/auth_providers/mod.rs b/ssr/src/component/auth_providers/mod.rs index 3386d09a..7b378748 100644 --- a/ssr/src/component/auth_providers/mod.rs +++ b/ssr/src/component/auth_providers/mod.rs @@ -202,7 +202,7 @@ mod server_fn_impl { referee_canister: Principal, ) -> Result<(), ServerFnError> { let canisters = unauth_canisters(); - let user = canisters.individual_user(referee_canister).await?; + let user = canisters.individual_user(referee_canister).await; let referrer_details = user .get_profile_details() .await? @@ -211,7 +211,7 @@ mod server_fn_impl { let referrer = canisters .individual_user(referrer_details.user_canister_id) - .await?; + .await; let user_details = user.get_profile_details().await?; @@ -251,7 +251,7 @@ mod server_fn_impl { use crate::{canister::user_index::Result_, state::admin_canisters::admin_canisters}; let admin_cans = admin_canisters(); - let user_idx = admin_cans.user_index_with(user_index).await?; + let user_idx = admin_cans.user_index_with(user_index).await; let res = user_idx .issue_rewards_for_referral( user_canister_id, @@ -271,15 +271,15 @@ mod server_fn_impl { user_canister: Principal, ) -> Result<bool, ServerFnError> { use crate::{ - canister::individual_user_template::{Result6, Result9, SessionType}, + canister::individual_user_template::{Result12, Result23, SessionType}, state::admin_canisters::admin_canisters, }; let admin_cans = admin_canisters(); - let user = admin_cans.individual_user_for(user_canister).await?; + let user = admin_cans.individual_user_for(user_canister).await; if matches!( user.get_session_type().await?, - Result6::Ok(SessionType::RegisteredSession) + Result12::Ok(SessionType::RegisteredSession) ) { return Ok(false); } @@ -287,8 +287,8 @@ mod server_fn_impl { .await .map_err(ServerFnError::from) .and_then(|res| match res { - Result9::Ok(_) => Ok(()), - Result9::Err(e) => Err(ServerFnError::new(format!( + Result23::Ok(_) => Ok(()), + Result23::Err(e) => Err(ServerFnError::new(format!( "failed to mark user as registered {e}" ))), })?; diff --git a/ssr/src/component/back_btn.rs b/ssr/src/component/back_btn.rs index 889a32cf..f5e9446d 100644 --- a/ssr/src/component/back_btn.rs +++ b/ssr/src/component/back_btn.rs @@ -38,9 +38,12 @@ pub fn go_back_or_fallback(fallback: &str) { } #[component] -pub fn BackButton(#[prop(into)] fallback: String) -> impl IntoView { +pub fn BackButton(#[prop(into)] fallback: MaybeSignal<String>) -> impl IntoView { view! { - <button on:click=move |_| go_back_or_fallback(&fallback) class="items-center"> + <button + on:click=move |_| go_back_or_fallback(&fallback.get_untracked()) + class="items-center" + > <Icon icon=icondata::AiLeftOutlined/> </button> } diff --git a/ssr/src/component/base_route.rs b/ssr/src/component/base_route.rs index ca8c4a91..9f52109f 100644 --- a/ssr/src/component/base_route.rs +++ b/ssr/src/component/base_route.rs @@ -100,10 +100,11 @@ fn CtxProvider(temp_identity: Option<JwkEcKey>, children: ChildrenFn) -> impl In .map(|res| { let cans_wire = try_or_redirect!(res); let cans = try_or_redirect!(cans_wire.canisters()); - - let (_, set_user_canister_id, _) = use_local_storage::<Option<Principal>, JsonSerdeCodec>(USER_CANISTER_ID_STORE); + let (_, set_user_canister_id, _) = use_local_storage::< + Option<Principal>, + JsonSerdeCodec, + >(USER_CANISTER_ID_STORE); set_user_canister_id(Some(cans.user_canister())); - canisters_store.set(Some(cans)); }) }} diff --git a/ssr/src/component/canisters_prov.rs b/ssr/src/component/canisters_prov.rs index b453b4b6..514b8753 100644 --- a/ssr/src/component/canisters_prov.rs +++ b/ssr/src/component/canisters_prov.rs @@ -23,13 +23,11 @@ where Some((children.get_value())(cans).into_view()) }; - view! { - <Suspense fallback=fallback>{loader}</Suspense> - } + view! { <Suspense fallback=fallback>{loader}</Suspense> } } #[component] -fn DataLoader<N, EF, D, DFut, DF>( +fn DataLoader<N, EF, D, St, DF>( cans: Canisters<true>, fallback: ViewFn, with: DF, @@ -38,19 +36,12 @@ fn DataLoader<N, EF, D, DFut, DF>( where N: IntoView + 'static, EF: Fn((Canisters<true>, D)) -> N + 'static + Clone, - DFut: Future<Output = D>, D: Serializable + Clone + 'static, - DF: Fn(Canisters<true>) -> DFut + 'static + Clone, + St: 'static + Clone, + DF: FnOnce(Canisters<true>) -> Resource<St, D> + 'static + Clone, { let can_c = cans.clone(); - let with_res = create_resource( - || (), - move |_| { - let cans = can_c.clone(); - let with = with.clone(); - async move { (with)(cans).await } - }, - ); + let with_res = (with)(can_c); let cans = store_value(cans.clone()); let children = store_value(children); @@ -58,16 +49,21 @@ where view! { <Suspense fallback=fallback> {move || { - with_res() - .map(move |d| (children.get_value())((cans.get_value(), d)).into_view()) + with_res().map(move |d| (children.get_value())((cans.get_value(), d)).into_view()) }} </Suspense> } } +pub fn with_cans<D: Serializable + Clone + 'static, DFut: Future<Output = D> + 'static>( + with: impl Fn(Canisters<true>) -> DFut + 'static + Clone, +) -> impl FnOnce(Canisters<true>) -> Resource<(), D> + Clone { + move |cans: Canisters<true>| create_resource(|| (), move |_| (with.clone())(cans.clone())) +} + #[component] -pub fn WithAuthCans<N, EF, D, DFut, DF>( +pub fn WithAuthCans<N, EF, D, St, DF>( #[prop(into, optional)] fallback: ViewFn, with: DF, children: EF, @@ -75,9 +71,9 @@ pub fn WithAuthCans<N, EF, D, DFut, DF>( where N: IntoView + 'static, EF: Fn((Canisters<true>, D)) -> N + 'static + Clone, - DFut: Future<Output = D>, + St: 'static + Clone, D: Serializable + Clone + 'static, - DF: Fn(Canisters<true>) -> DFut + 'static + Clone, + DF: FnOnce(Canisters<true>) -> Resource<St, D> + 'static + Clone, { view! { <AuthCansProvider fallback=fallback.clone() let:cans> diff --git a/ssr/src/component/claim_tokens.rs b/ssr/src/component/claim_tokens.rs new file mode 100644 index 00000000..8f3ec55d --- /dev/null +++ b/ssr/src/component/claim_tokens.rs @@ -0,0 +1,51 @@ +use candid::Principal; +use leptos::*; + +use crate::{ + state::canisters::authenticated_canisters, + utils::route::failure_redirect, + utils::{time::sleep, token::claim_tokens_from_first_neuron_if_required}, +}; +use web_time::Duration; + +#[component] +pub fn ClaimTokensOrRedirectError(token_root: Principal) -> impl IntoView { + let auth_cans = authenticated_canisters(); + let claim_res = auth_cans.derive( + || (), + move |cans_wire, _| async move { + log::debug!("Claim token for {token_root}"); + let cans_wire = cans_wire?; + loop { + let res = + claim_tokens_from_first_neuron_if_required(cans_wire.clone(), token_root).await; + match res { + Ok(_) => return Ok(()), + Err(ServerFnError::ServerError(e)) if e.contains("PreInitializationSwap") => { + log::warn!("Governance is not ready. Retrying..."); + sleep(Duration::from_secs(8)).await; + continue; + } + Err(e) => return Err(e), + } + } + }, + ); + + view! { + <Suspense> + {move || { + claim_res() + .map(|res| { + match res { + Ok(_) => {} + Err(e) => { + failure_redirect(e); + } + } + }) + }} + + </Suspense> + } +} diff --git a/ssr/src/component/login_modal.rs b/ssr/src/component/login_modal.rs index c76207e5..b52101f2 100644 --- a/ssr/src/component/login_modal.rs +++ b/ssr/src/component/login_modal.rs @@ -1,11 +1,17 @@ -use super::{auth_providers::LoginProviders, overlay::ShadowOverlay}; +use super::{ + auth_providers::LoginProviders, + overlay::{ShadowOverlay, ShowOverlay}, +}; use leptos::*; #[component] pub fn LoginModal(#[prop(into)] show: RwSignal<bool>) -> impl IntoView { let lock_closing = create_rw_signal(false); view! { - <ShadowOverlay show lock_closing> + <ShadowOverlay show=ShowOverlay::MaybeClosable { + show, + closable: lock_closing, + }> <LoginProviders show_modal=show lock_closing/> </ShadowOverlay> } diff --git a/ssr/src/component/mod.rs b/ssr/src/component/mod.rs index 055845f9..6642eaad 100644 --- a/ssr/src/component/mod.rs +++ b/ssr/src/component/mod.rs @@ -4,6 +4,7 @@ pub mod back_btn; pub mod base_route; pub mod bullet_loader; pub mod canisters_prov; +pub mod claim_tokens; pub mod coming_soon; pub mod connect; pub mod content_upload; @@ -17,12 +18,16 @@ pub mod login_modal; pub mod modal; pub mod nav; pub mod nav_icons; +pub mod onboarding_flow; pub mod option; pub mod overlay; pub mod profile_placeholders; pub mod scrolling_post_view; +pub mod share_popup; pub mod social; pub mod spinner; pub mod title; pub mod toggle; +pub mod token_confetti_symbol; +pub mod token_logo_sanitize; pub mod video_player; diff --git a/ssr/src/component/nav.rs b/ssr/src/component/nav.rs index 32880545..2a29251b 100644 --- a/ssr/src/component/nav.rs +++ b/ssr/src/component/nav.rs @@ -2,7 +2,6 @@ use super::nav_icons::*; use leptos::*; use leptos_icons::*; use leptos_router::*; - #[component] fn NavIcon( idx: usize, @@ -12,13 +11,13 @@ fn NavIcon( cur_selected: Memo<usize>, ) -> impl IntoView { view! { - <a href=href class="flex items-center justify-center"> + <a href=href class="flex justify-center items-center"> <Show when=move || cur_selected() == idx fallback=move || { view! { <div class="py-5"> - <Icon icon=icon class="text-white text-2xl md:text-3xl"/> + <Icon icon=icon class="text-2xl text-white md:text-3xl"/> </div> } } @@ -27,7 +26,7 @@ fn NavIcon( <div class="py-5 border-t-2 border-t-pink-500"> <Icon icon=filled_icon.unwrap_or(icon) - class="text-white aspect-square text-2xl md:text-3xl" + class="text-2xl text-white md:text-3xl aspect-square" /> </div> </Show> @@ -35,43 +34,43 @@ fn NavIcon( } } -#[component] -fn TrophyIcon(idx: usize, cur_selected: Memo<usize>) -> impl IntoView { - view! { - <a href="/leaderboard" class="flex items-center justify-center"> - <Show - when=move || cur_selected() == idx - fallback=move || { - view! { - <div class="py-5"> - <Icon icon=TrophySymbol class="text-white fill-none text-2xl md:text-3xl"/> - </div> - } - } - > - - <div class="py-5 border-t-2 border-t-pink-500"> - <Icon - icon=TrophySymbolFilled - class="text-white fill-none aspect-square text-2xl md:text-3xl" - /> - </div> - </Show> - </a> - } -} +// #[component] +// fn TrophyIcon(idx: usize, cur_selected: Memo<usize>) -> impl IntoView { +// view! { +// <a href="/leaderboard" class="flex justify-center items-center"> +// <Show +// when=move || cur_selected() == idx +// fallback=move || { +// view! { +// <div class="py-5"> +// <Icon icon=TrophySymbol class="text-2xl text-white md:text-3xl fill-none"/> +// </div> +// } +// } +// > +// +// <div class="py-5 border-t-2 border-t-pink-500"> +// <Icon +// icon=TrophySymbolFilled +// class="text-2xl text-white md:text-3xl fill-none aspect-square" +// /> +// </div> +// </Show> +// </a> +// } +// } #[component] fn UploadIcon(idx: usize, cur_selected: Memo<usize>) -> impl IntoView { view! { - <a href="/upload" class="flex items-center justify-center rounded-full text-white"> + <a href="/upload" class="flex justify-center items-center text-white rounded-full"> <Show when=move || cur_selected() == idx fallback=move || { view! { <Icon icon=icondata::AiPlusOutlined - class="rounded-full bg-transparent h-10 w-10 border-2 p-2" + class="p-2 w-10 h-10 bg-transparent rounded-full border-2" /> } } @@ -80,9 +79,9 @@ fn UploadIcon(idx: usize, cur_selected: Memo<usize>) -> impl IntoView { <div class="border-t-2 border-transparent"> <Icon icon=icondata::AiPlusOutlined - class="bg-primary-600 rounded-full aspect-square h-10 w-10 p-2" + class="p-2 w-10 h-10 rounded-full bg-primary-600 aspect-square" /> - <div class="absolute bottom-0 bg-primary-600 w-10 blur-md"></div> + <div class="absolute bottom-0 w-10 bg-primary-600 blur-md"></div> </div> </Show> </a> @@ -97,22 +96,25 @@ pub fn NavBar() -> impl IntoView { let path = cur_location.pathname.get(); match path.as_str() { "/" => 0, - "/leaderboard" => 1, + // "/leaderboard" => 1, "/upload" => 2, "/wallet" | "/transactions" => 3, - "/menu" => 4, - s if s.starts_with("/your-profile") => 4, + "/menu" | "/leaderboard" => 4, + "/board" => 0, s if s.starts_with("/hot-or-not") => { home_path.set(path); 0 } s if s.starts_with("/profile") => 0, + s if s.starts_with("/token/info") => 3, + s if s.starts_with("/token/create") => 2, + s if s.starts_with("/your-profile") => 5, _ => 4, } }); view! { - <div class="fixed z-50 bottom-0 left-0 flex flex-row justify-between px-6 items-center w-full bg-black/80"> + <div class="flex fixed bottom-0 left-0 z-50 flex-row justify-between items-center px-6 w-full bg-black/80"> <NavIcon idx=0 href=home_path @@ -120,8 +122,6 @@ pub fn NavBar() -> impl IntoView { filled_icon=HomeSymbolFilled cur_selected=cur_selected /> - <TrophyIcon idx=1 cur_selected/> - <UploadIcon idx=2 cur_selected/> <NavIcon idx=3 href="/wallet" @@ -129,6 +129,14 @@ pub fn NavBar() -> impl IntoView { filled_icon=WalletSymbolFilled cur_selected=cur_selected /> + <UploadIcon idx=2 cur_selected/> + <NavIcon + idx=5 + href="/your-profile" + icon=ProfileIcon + filled_icon=ProfileIconFilled + cur_selected=cur_selected + /> <NavIcon idx=4 href="/menu" icon=MenuSymbol cur_selected=cur_selected/> </div> } diff --git a/ssr/src/component/nav_icons.rs b/ssr/src/component/nav_icons.rs index 54536224..96d2af5d 100644 --- a/ssr/src/component/nav_icons.rs +++ b/ssr/src/component/nav_icons.rs @@ -47,3 +47,20 @@ icon_gen!( <path fill="currentColor" stroke="currentColor" stroke-width="1.5" d="M4.75 1.88c0-.63.5-1.13 1.13-1.13h11.25c.62 0 1.12.5 1.12 1.13V7.5a6.75 6.75 0 0 1-13.5 0V1.87v.01Zm2 17.79c0 .32.26.58.58.58h8.34c.32 0 .58-.26.58-.58 0-1.06-.86-1.92-1.92-1.92H8.67c-1.06 0-1.92.86-1.92 1.92Z"></path> <path fill="currentColor" d="M11 17h2v-3h-2v3Z"></path>"### ); +icon_gen!( + ProfileIcon, + view_box = "0 0 512 512", + r###"<g> + <path d="M419.5 512H92.4c-32-.1-57.9-26-58-58v-25.6c0-62.6 26.3-122.3 72.6-164.4 19.3-17.5 48-19.9 69.9-5.8 22.9 14.7 50.2 22.5 79.1 22.5s56.2-7.8 79.1-22.5c21.9-14.1 50.6-11.7 69.9 5.8 46.3 42.1 72.7 101.8 72.6 164.4V454c-.1 32.1-26.1 58-58.1 58zM145.7 269c-9.4 0-18.4 3.5-25.3 9.8-42.1 38.3-66.1 92.7-66.1 149.6V454c.1 21 17 37.9 38 38h327.1c21 0 38-17 38-38v-25.6c0-56.9-24-111.2-66.1-149.6-12.6-11.4-31.3-13-45.6-3.8-26.1 16.8-57.2 25.7-89.9 25.7s-63.8-8.9-89.9-25.7c-5.9-3.9-13-6-20.2-6z" fill="currentColor" /> + <path d="M256 258c-71.2-.1-128.9-57.8-129-129C127 57.9 184.8 0 256 0c71.2.1 128.9 57.8 129 129-.1 71.1-57.9 129-129 129zm0-237.9c-60.2 0-109 48.8-109 109s48.8 109 109 109 109-48.8 109-109c-.1-60.2-48.8-109-109-109z" fill="currentColor" /> + </g>"### +); + +icon_gen!( + ProfileIconFilled, + view_box = "0 0 512 512", + r###"<g> + <path d="M256 292.1c33 0 63.5-9.3 87.9-25.1 18.9-12.1 43.5-10.1 60.1 5 46.1 41.8 72.3 101.1 72.2 163.4v26.7c0 27.6-22.4 49.9-50 49.9H85.8c-27.6 0-50-22.3-50-49.9v-26.7c-.2-62.2 26-121.6 72.1-163.3 16.6-15.1 41.3-17.1 60.1-5 24.5 15.7 54.9 25 88 25z" fill="currentColor" /> + <circle cx="256" cy="123.8" r="123.8" fill="currentColor" /> + </g>"### +); diff --git a/ssr/src/component/onboarding_flow.rs b/ssr/src/component/onboarding_flow.rs new file mode 100644 index 00000000..de027ee5 --- /dev/null +++ b/ssr/src/component/onboarding_flow.rs @@ -0,0 +1,233 @@ +use leptos::*; +use leptos_icons::Icon; + +#[component] +pub fn OnboardingPopUp(onboard_on_click: WriteSignal<bool>) -> impl IntoView { + let onboarding_page_no = create_rw_signal(1); + + let style = move || { + if onboarding_page_no.get() == 2 { + "background: radial-gradient(circle at 50% calc(100% - 148px), transparent 40px, rgba(0, 0, 0, 0.7) 37px);" + } else if onboarding_page_no.get() == 3 { + "background: radial-gradient(circle at calc(50% - 90px) calc(100% - 130px), transparent 56px, rgba(0, 0, 0, 0.7) 51px);" + } else if onboarding_page_no.get() == 4 { + "background: radial-gradient(circle at calc(50% + 90px) calc(100% - 130px), transparent 56px, rgba(0, 0, 0, 0.7) 51px);" + } else { + "" + } + }; + + view! { + <div + class="h-full w-full bg-black bg-opacity-70 z-10 flex flex-col justify-center relative" + style=style + > + <Show when=move || { onboarding_page_no.get() == 1 }> + <OnboardingTopDecorator/> + <div class="flex flex-row justify-center"> + <div class="flex flex-col justify-center w-9/12 sm:w-4/12 relative gap-y-36"> + <div class="relative self-center"> + <p class="text-white text-center font-bold w-56 text-2xl leading-normal"> + A new Hot or Not game experience awaits you + </p> + <img + class="-left-6 top-8 h-5 w-5 absolute" + src="/img/decorator/star.svg" + /> + <img + class="-left-2 -top-6 h-4 w-4 absolute" + src="/img/decorator/star.svg" + /> + <img + class="left-6 -top-2 h-3 w-3 absolute" + src="/img/decorator/star.svg" + /> + <img + class="-right-6 -top-2 h-6 w-6 absolute" + src="/img/decorator/star.svg" + /> + <img + class="right-2 -top-1 h-2 w-2 absolute" + src="/img/decorator/star.svg" + /> + <img + class="-right-5 bottom-4 h-2 w-2 absolute" + src="/img/decorator/star.svg" + /> + </div> + <div class="flex flex-col items-center gap-y-4"> + <button + class="self-center font-semibold rounded-full bg-primary-600 py-2 md:py-3 w-full max-w-80 text-center text-base md:text-xl text-white" + on:click=move |_| onboarding_page_no.set(2) + > + Start Tutorial + </button> + <button + class="text-white text-center font-medium text-base leading-normal font-sans" + on:click=move |_| onboard_on_click.set(true) + > + Skip Tutorial + </button> + </div> + </div> + </div> + </Show> + + <Show when=move || { onboarding_page_no.get() == 2 }> + <OnboardingTopCross onboard_on_click/> + <OnboardingContent + header_text="Select your bet amount" + body_text="Select your bet (50, 100, or 200) by tapping the coin or arrows" + onboarding_page_no + /> + </Show> + + <Show when=move || { onboarding_page_no.get() == 3 }> + <OnboardingTopCross onboard_on_click/> + <OnboardingContent + header_text="Place your first bet" + body_text="Do you think the video will be popular? Click 'Hot' and place your bet" + onboarding_page_no + /> + </Show> + + <Show when=move || { onboarding_page_no.get() == 4 }> + <OnboardingTopCross onboard_on_click/> + <OnboardingContent + header_text="Place your first bet" + body_text="If you think video won't be popular, click 'Not' and place your bet" + onboarding_page_no + /> + </Show> + + <Show when=move || { onboarding_page_no.get() == 5 }> + <OnboardingTopDecorator/> + <div class="flex flex-row justify-center"> + <div class="flex flex-col justify-center w-9/12 sm:w-4/12 relative"> + <div class="self-center"> + <p class="text-white text-center font-bold w-56 text-2xl leading-normal"> + "There's even more" + </p> + </div> + <div class="flex flex-col justify-center gap-y-3 mt-12"> + <div class="self-center"> + <img src="/img/decorator/buy_coin.svg"/> + </div> + <div class="self-center"> + <p class="text-white text-center font-medium text-sm leading-normal"> + Refer and get COYNS + </p> + </div> + </div> + <div class="flex flex-col justify-center gap-y-3 mt-12"> + <div class="self-center"> + <img src="/img/decorator/prizes.svg"/> + </div> + <div class="self-center"> + <p class="text-white text-center font-medium text-sm leading-normal"> + Play and earn + </p> + </div> + </div> + <button + class="font-bold rounded-full bg-primary-600 py-3 md:py-4 w-80 mt-24 self-center text-center text-lg md:text-xl text-white" + on:click=move |_| onboard_on_click.set(true) + > + "Let's make some money" + </button> + </div> + </div> + </Show> + </div> + } +} + +#[component] +pub fn OnboardingTopDecorator() -> impl IntoView { + view! { + <div class="top-0 w-full flex justify-center"> + <div class="absolute left-0 top-0"> + <img src="/img/decorator/decore-left.svg"/> + </div> + <div class="absolute right-0 top-0"> + <img src="/img/decorator/decore-right.svg"/> + </div> + </div> + } +} + +#[component] +pub fn OnboardingTopCross(onboard_on_click: WriteSignal<bool>) -> impl IntoView { + view! { + <div class="top-0 w-full flex justify-center"> + <div class="absolute right-[16.1px] top-[19px]"> + <button + class="text-white bg-transparent bg-opacity-70" + on:click=move |_| onboard_on_click.set(true) + > + <Icon class="w-[24px] h-[24px]" icon=icondata::ChCross/> + </button> + </div> + </div> + } +} + +#[component] +pub fn OnboardingContent( + header_text: &'static str, + body_text: &'static str, + onboarding_page_no: RwSignal<i32>, +) -> impl IntoView { + view! { + <div class="flex flex-row justify-center"> + <div class="flex flex-col justify-center w-9/12 sm:w-4/12 relative"> + <div class="relative flex flex-col justify-center items-center gap-y-9"> + <div class="flex flex-col gap-y-2 justify-center items-center"> + <div class="self-center"> + <p class="text-white text-center font-bold w-72 text-2xl leading-normal -mt-3"> + {header_text} + </p> + </div> + <div class="self-center px-2"> + <p class="text-white text-center font-medium w-64 text-sm leading-5 font-sans"> + {body_text} + </p> + </div> + </div> + <div class="flex flex-col gap-y-4 justify-center items-center"> + <button + class="self-center font-semibold rounded-full bg-primary-600 z-20 py-2 md:py-3 w-40 max-w-30 text-center text-base md:text-xl text-white" + on:click=move |_| onboarding_page_no.update(|page| *page += 1) + > + Next + </button> + <button + class="text-white text-center font-semibold text-lg sm:text-base z-20 leading-normal font-sans" + on:click=move |_| onboarding_page_no.update(|page| *page -= 1) + > + Previous + </button> + </div> + <Show when=move || { onboarding_page_no.get() == 2 }> + <img + src="/img/decorator/coin_arrow.svg" + class="absolute h-[30vh] hot-left-arrow -ml-56 sm:-ml-64 mt-48 sm:mt:64" + /> + </Show> + <Show when=move || { onboarding_page_no.get() == 3 }> + <img + src="/img/decorator/hot_arrow.svg" + class="absolute h-[33vh] hot-left-arrow -ml-60 sm:-ml-72 mt-48 sm:mt-64" + /> + </Show> + <Show when=move || { onboarding_page_no.get() == 4 }> + <img + src="/img/decorator/not_arrow.svg" + class="absolute h-[33vh] hot-left-arrow ml-60 sm:ml-72 mt-48 sm:mt-64" + /> + </Show> + </div> + </div> + </div> + } +} diff --git a/ssr/src/component/overlay.rs b/ssr/src/component/overlay.rs index d90eafa9..b587279a 100644 --- a/ssr/src/component/overlay.rs +++ b/ssr/src/component/overlay.rs @@ -1,33 +1,184 @@ +use super::spinner::Spinner; use leptos::*; +#[derive(Clone, Copy)] +pub enum ShowOverlay { + /// Show overlay and allow closing by user + Closable(RwSignal<bool>), + MaybeClosable { + show: RwSignal<bool>, + /// Allow closing based on this signal + closable: RwSignal<bool>, + }, + /// Show overlay but prevent closing by user + AlwaysLocked(Signal<bool>), +} + +impl From<bool> for ShowOverlay { + fn from(b: bool) -> Self { + ShowOverlay::Closable(RwSignal::new(b)) + } +} + +impl From<RwSignal<bool>> for ShowOverlay { + fn from(s: RwSignal<bool>) -> Self { + ShowOverlay::Closable(s) + } +} + +impl From<Signal<bool>> for ShowOverlay { + fn from(s: Signal<bool>) -> Self { + ShowOverlay::AlwaysLocked(s) + } +} + +impl SignalGet for ShowOverlay { + type Value = bool; + + fn get(&self) -> bool { + match self { + ShowOverlay::Closable(s) => s.get(), + ShowOverlay::AlwaysLocked(s) => s.get(), + ShowOverlay::MaybeClosable { show, .. } => show.get(), + } + } + + fn try_get(&self) -> Option<bool> { + match self { + ShowOverlay::Closable(s) => s.try_get(), + ShowOverlay::AlwaysLocked(s) => s.try_get(), + ShowOverlay::MaybeClosable { show, .. } => show.try_get(), + } + } +} + +impl SignalSet for ShowOverlay { + type Value = bool; + + fn set(&self, value: bool) { + match self { + ShowOverlay::Closable(s) => s.set(value), + ShowOverlay::AlwaysLocked(_) => {} + ShowOverlay::MaybeClosable { show, closable } => { + if closable.get_untracked() { + show.set(value); + } + } + } + } + + fn try_set(&self, value: bool) -> Option<bool> { + match self { + ShowOverlay::Closable(s) => s.try_set(value), + ShowOverlay::AlwaysLocked(_) => None, + ShowOverlay::MaybeClosable { show, closable } => { + if closable.try_get_untracked()? { + show.try_set(value) + } else { + None + } + } + } + } +} + #[component] -pub fn ShadowOverlay( - #[prop(into)] show: RwSignal<bool>, - #[prop(into, optional)] lock_closing: RwSignal<bool>, - children: ChildrenFn, -) -> impl IntoView { - let _lock_closing = lock_closing; +pub fn ShadowOverlay(#[prop(into)] show: ShowOverlay, children: ChildrenFn) -> impl IntoView { + let children_s = store_value(children); view! { - <Show when=show> - <div - on:click={ - #[cfg(feature = "hydrate")] - { - move |ev| { - use web_sys::HtmlElement; - let target = event_target::<HtmlElement>(&ev); - if target.class_list().contains("modal-bg") && !_lock_closing() { - show.set(false); + <Show when=move || show.get()> + // Portal is necessary + // see more: https://stackoverflow.com/questions/28157125/why-does-transform-break-position-fixed/28157774#28157774 + <Portal> + <div + on:click={ + #[cfg(feature = "hydrate")] + { + move |ev| { + use web_sys::HtmlElement; + let target = event_target::<HtmlElement>(&ev); + if target.class_list().contains("modal-bg") { + show.set(false); + } } } + #[cfg(not(feature = "hydrate"))] { |_| () } } - #[cfg(not(feature = "hydrate"))] { |_| () } - } - class="flex cursor-pointer modal-bg w-dvw h-dvh fixed left-0 top-0 bg-black/60 z-[99] justify-center items-center overflow-hidden" - > + class="flex cursor-pointer modal-bg w-dvw h-dvh fixed left-0 top-0 bg-black/60 z-[99] justify-center items-center overflow-hidden" + > + {(children_s())()} + </div> + </Portal> + </Show> + } +} + +#[component] +fn ActionRunningOverlay(message: String) -> impl IntoView { + view! { + <div class="w-full h-full flex flex-col gap-6 items-center justify-center text-white text-center text-xl font-semibold"> + <Spinner/> + <span>{message}</span> + <span>Please wait...</span> + </div> + } +} + +#[component] +pub fn PopupOverlay(#[prop(into)] show: ShowOverlay, children: ChildrenFn) -> impl IntoView { + view! { + <ShadowOverlay show> + <div class="px-4 pt-4 pb-12 mx-6 w-full lg:w-1/2 max-h-[65%] rounded-xl bg-white"> {children()} </div> - </Show> + </ShadowOverlay> + } +} + +/// Tracks an action's progress and shows a modal with the result +/// action -> The action to track +/// loading_message -> The message to show while the action is pending +/// modal -> The modal to show when the action is done +/// close -> Set this signal to true to close the modal (automatically reset upon closing) +#[component] +pub fn ActionTrackerPopup< + S: 'static, + R: 'static + Clone, + V: IntoView, + IV: Fn(R) -> V + Clone + 'static, +>( + action: Action<S, R>, + #[prop(into)] loading_message: String, + modal: IV, + #[prop(optional, into)] close: RwSignal<bool>, +) -> impl IntoView { + let pending = action.pending(); + let action_value = action.value(); + let res = Signal::derive(move || { + if pending() { + return None; + } + action_value() + }); + let show_popup = Signal::derive(move || { + let show = (pending() || res.with(|r| r.is_some())) && !close(); + close.set_untracked(false); + show + }); + let modal_s = store_value(modal); + let loading_msg_s = store_value(loading_message); + + view! { + <ShadowOverlay show=show_popup> + <Show + when=move || res.with(|r| r.is_some()) + fallback=move || view! { <ActionRunningOverlay message=loading_msg_s.get_value()/> } + > + <div class="px-4 pt-4 pb-12 mx-6 w-full lg:w-1/2 max-h-[65%] rounded-xl bg-white"> + {move || (modal_s.get_value())(res().unwrap())} + </div> + </Show> + </ShadowOverlay> } } diff --git a/ssr/src/component/scrolling_post_view.rs b/ssr/src/component/scrolling_post_view.rs index 48c321c0..f45f9c07 100644 --- a/ssr/src/component/scrolling_post_view.rs +++ b/ssr/src/component/scrolling_post_view.rs @@ -15,10 +15,7 @@ pub fn MuteIconOverlay(show_mute_icon: RwSignal<bool>) -> impl IntoView { class="fixed top-1/2 left-1/2 z-20 cursor-pointer pointer-events-none" on:click=move |_| AudioState::toggle_mute() > - <Icon - class="text-white/80 animate-ping text-4xl" - icon=icondata::BiVolumeMuteSolid - /> + <Icon class="text-white/80 animate-ping text-4xl" icon=icondata::BiVolumeMuteSolid/> </button> </Show> } @@ -72,7 +69,7 @@ pub fn ScrollingPostView<F: Fn() -> V + Clone + 'static, V>( return; } if video_queue.with_untracked(|q| q.len()).saturating_sub(queue_idx) - <= threshold_trigger_fetch + <= threshold_trigger_fetch { next_videos.as_ref().map(|nv| { nv() }); } @@ -98,7 +95,12 @@ pub fn ScrollingPostView<F: Fn() -> V + Clone + 'static, V>( <div _ref=container_ref class="snap-always snap-end w-full h-full"> <Show when=show_video> <BgView video_queue current_idx idx=queue_idx> - <VideoViewForQueue video_queue current_idx idx=queue_idx muted /> + <VideoViewForQueue + video_queue + current_idx + idx=queue_idx + muted + /> </BgView> </Show> </div> diff --git a/ssr/src/component/share_popup.rs b/ssr/src/component/share_popup.rs new file mode 100644 index 00000000..038d8070 --- /dev/null +++ b/ssr/src/component/share_popup.rs @@ -0,0 +1,136 @@ +use leptos::*; +use leptos_icons::*; + +use crate::{ + component::overlay::*, + utils::web::{copy_to_clipboard, share_url}, +}; + +#[component] +fn ShareContent( + share_link: String, + message: String, + #[prop(into)] show_popup: SignalSetter<bool>, +) -> impl IntoView { + // let has_share_support = check_share_support(); + + let share_link_social = share_link.clone(); + + // Encode the message for URLs + let copy = share_link.clone(); + let copy_clipboard = move |_| { + copy_to_clipboard(©); + }; + view! { + <div class="flex flex-col gap-6 items-center p-6 w-full h-full bg-white rounded-lg shadow-lg"> + <div class="flex flex-col gap-2 items-center"> + <img + class="w-16 h-16 md:w-20 md:h-20" + src="/img/android-chrome-384x384.png" + alt="YRAL Logo" + /> + + <span class="text-xl font-semibold text-center md:text-2xl">Share this app</span> + </div> + <SocialShare message=message.clone() share_link=share_link_social.clone()/> + <div class="flex overflow-x-auto justify-center items-center px-10 mx-1 space-x-2 w-full rounded-xl border-2 border-neutral-700 h-[2.5rem] md:h-[5rem]"> + <span class="text-lg text-black md:text-xl truncate">{&share_link.clone()}</span> + <button on:click=copy_clipboard> + <Icon class="w-6 h-6 text-black cursor-pointer" icon=icondata::BiCopyRegular/> + </button> + </div> + <button + on:click=move |_| show_popup.set(false) + class="py-4 w-3/4 text-lg text-center text-white rounded-full bg-primary-600" + > + Back + </button> + + </div> + } +} + +#[component] +fn SocialShare(share_link: String, message: String) -> impl IntoView { + let encoded_message = urlencoding::encode(&message); + // let encoded_link = urlencoding::encode(&profile_link); + + // Facebook share URL using Dialog API + let fb_url = format!( + "http://www.facebook.com/share.php?u={}"e={}", + share_link, encoded_message + ); + + // WhatsApp share URL + let whatsapp_url = format!("https://wa.me/?text={}", encoded_message); + + // Twitter share URL + let twitter_url = format!("https://twitter.com/intent/tweet?text={}", encoded_message); + + let telegram_url = format!("https://telegram.me/share/url?url={}", &share_link); + + // LinkedIn share URL + let linkedin_url = format!( + "https://linkedin.com/sharing/share-offsite/?url={}&title={}", + &share_link, encoded_message + ); + + view! { + <div class="flex gap-4"> + // Facebook button + <a href=fb_url target="_blank"> + <Icon class="text-3xl md:text-4xl text-primary-600" icon=icondata::BsFacebook/> + </a> + + // Twitter button + <a href=twitter_url target="_blank"> + <Icon class="text-3xl md:text-4xl text-primary-600" icon=icondata::BsTwitterX/> + </a> + + // WhatsApp button + <a href=whatsapp_url target="_blank"> + <Icon + class="text-3xl md:text-4xl text-primary-600" + icon=icondata::FaSquareWhatsappBrands + /> + </a> + + // LinkedIn button + <a href=linkedin_url target="_blank"> + <Icon class="text-3xl md:text-4xl text-primary-600" icon=icondata::TbBrandLinkedin/> + </a> + <a href=telegram_url target="_blank"> + <Icon class="text-3xl md:text-4xl text-primary-600" icon=icondata::TbBrandTelegram/> + </a> + </div> + } +} + +#[component] +pub fn ShareButtonWithFallbackPopup(share_link: String, message: String) -> impl IntoView { + let show_fallback = create_rw_signal(false); + let share_link_c = share_link.clone(); + let on_share_click = move |ev: ev::MouseEvent| { + ev.stop_propagation(); + if share_url(&share_link_c).is_none() { + show_fallback.set(true); + } + }; + + view! { + <button + on:click=on_share_click + class="text-white text-center p-1 text-lg md:text-xl bg-primary-600 rounded-full flex items-center justify-center" + > + <Icon icon=icondata::AiShareAltOutlined/> + + </button> + <PopupOverlay show=show_fallback> + <ShareContent + share_link=share_link.clone() + message=message.clone() + show_popup=show_fallback + /> + </PopupOverlay> + } +} diff --git a/ssr/src/component/spinner.rs b/ssr/src/component/spinner.rs index 96150661..f096a94c 100644 --- a/ssr/src/component/spinner.rs +++ b/ssr/src/component/spinner.rs @@ -12,7 +12,7 @@ pub fn Spinner() -> impl IntoView { #[component] pub fn SpinnerFit() -> impl IntoView { view! { - <div class="animate-spin border-solid rounded-full border-t-transparent border-primary-600 border-8 w-full h-full"/> + <div class="animate-spin border-solid rounded-full border-t-transparent border-primary-600 border-8 w-full h-full"></div> } } diff --git a/ssr/src/component/title.rs b/ssr/src/component/title.rs index 5a7f4123..51d03845 100644 --- a/ssr/src/component/title.rs +++ b/ssr/src/component/title.rs @@ -10,7 +10,7 @@ pub fn Title( ) -> impl IntoView { view! { <span - class="sticky top-0 bg-black text-white p-4 w-full items-center z-50" + class="sticky top-0 bg-transparent text-white p-4 w-full items-center z-50" class:justify-center=justify_center class:flex=justify_center > diff --git a/ssr/src/component/token_confetti_symbol.rs b/ssr/src/component/token_confetti_symbol.rs new file mode 100644 index 00000000..88e8a9e6 --- /dev/null +++ b/ssr/src/component/token_confetti_symbol.rs @@ -0,0 +1,103 @@ +use leptos::*; + +#[component] +pub fn TokenConfettiSymbol(#[prop(into)] class: String) -> impl IntoView { + view! { + <svg class=class viewBox="0 0 254 175" fill="none" xmlns="http://www.w3.org/2000/svg"> + <g clip-path="url(#clip0_433_5816)"> + <path + d="M136.908 166.009C177.777 166.009 210.908 132.878 210.908 92.0089C210.908 51.1398 177.777 18.0089 136.908 18.0089C96.0391 18.0089 62.9082 51.1398 62.9082 92.0089C62.9082 132.878 96.0391 166.009 136.908 166.009Z" + fill="#FED056" + ></path> + <path + d="M136.908 152.307C170.21 152.307 197.206 125.311 197.206 92.0089C197.206 58.707 170.21 31.7104 136.908 31.7104C103.606 31.7104 76.6094 58.707 76.6094 92.0089C76.6094 125.311 103.606 152.307 136.908 152.307Z" + fill="#FEB635" + ></path> + <g filter="url(#filter0_d_433_5816)"> + <path + fill-rule="evenodd" + clip-rule="evenodd" + d="M134.39 123.176C134.067 123.176 133.741 123.119 133.421 123.003C132.197 122.558 131.432 121.334 131.573 120.039L133.749 99.9426H117.075C116.03 99.9426 115.069 99.3674 114.576 98.4437C114.083 97.5201 114.14 96.4037 114.721 95.5339L137.07 62.1006C137.795 61.0154 139.167 60.5649 140.393 61.0154C141.62 61.4602 142.382 62.6842 142.241 63.9791L140.065 84.0759H156.742C157.787 84.0759 158.748 84.6511 159.241 85.5747C159.731 86.4984 159.677 87.6147 159.096 88.4846L136.744 121.918C136.209 122.723 135.313 123.176 134.39 123.176Z" + fill="white" + ></path> + </g> + <path + d="M197.206 92.0089C197.206 125.251 170.15 152.307 136.908 152.307C126.068 152.307 115.922 149.446 107.134 144.416C115.228 148.463 124.363 150.746 134.017 150.746C167.259 150.746 194.316 123.69 194.316 90.448C194.316 68.0456 182.002 48.4472 163.791 38.0698C183.591 47.9558 197.206 68.3925 197.206 92.0089ZM141.013 34.5433C152.893 34.5433 163.964 38.012 173.272 43.9378C163.155 36.3066 150.552 31.7394 136.908 31.7394C103.666 31.7394 76.6094 58.7667 76.6094 92.0378C76.6094 113.486 87.8828 132.391 104.822 143.028C90.1953 132.015 80.7141 114.527 80.7141 94.8417C80.7141 61.5995 107.77 34.5433 141.013 34.5433Z" + fill="#FC9924" + ></path> + </g> + <path + d="M12.1219 35.2554C12.4646 35.113 12.8556 35.1589 13.1614 35.3791L17.3589 38.2654C18.1228 38.7998 19.1601 38.1948 19.1026 37.2587L18.8099 32.1008C18.786 31.718 18.9435 31.3635 19.2417 31.1293L23.2389 27.9511C23.9767 27.374 23.7292 26.184 22.8312 25.9516L17.9336 24.6406C17.5659 24.5422 17.287 24.279 17.1566 23.9261L15.4259 19.0696C15.1075 18.1792 13.9252 18.0449 13.4265 18.8352L10.6856 23.1864C10.4917 23.5003 10.1479 23.6976 9.77804 23.7089L4.71155 23.8783C3.78229 23.9083 3.29223 25.011 3.88312 25.7424L7.10022 29.7345C7.33587 30.0252 7.41718 30.4187 7.3135 30.7742L5.91232 35.7437C5.65841 36.6469 6.52799 37.4674 7.40415 37.1203L12.1219 35.2554Z" + fill="#2AAD52" + ></path> + <path + d="M70.7113 32.1316C71.0669 31.9952 71.379 31.712 71.5482 31.3313C71.6307 31.1488 71.6767 30.9514 71.6837 30.7506C71.6907 30.5497 71.6586 30.3495 71.5891 30.1614C71.5196 29.9734 71.4141 29.8013 71.2789 29.655C71.1436 29.5088 70.9812 29.3914 70.801 29.3096C60.9329 24.7558 56.538 12.8639 61.0069 2.80804C61.3511 2.0335 61.0163 1.12754 60.2597 0.786326C59.5032 0.445108 58.6106 0.776755 58.2757 1.5477C53.1243 13.1395 58.1888 26.8433 69.5678 32.1023C69.9378 32.2651 70.3557 32.2681 70.7113 32.1316ZM24.635 83.5695C24.9906 83.4331 25.3027 83.15 25.4719 82.7692C29.9407 72.7134 41.6106 68.2349 51.4788 72.7888C52.2389 73.1395 53.1279 72.7984 53.4628 72.0274C53.7976 71.2565 53.4721 70.3469 52.7156 70.0057C41.3402 64.7563 27.8922 69.9171 22.7314 81.5125C22.3872 82.287 22.722 83.193 23.4786 83.5342C23.8615 83.703 24.2793 83.706 24.635 83.5695Z" + fill="#FED056" + ></path> + <path + d="M52.1711 35.8602C52.4706 35.7452 52.7254 35.5385 52.917 35.2473C53.3584 34.5334 53.1488 33.5903 52.4483 33.1405C43.4129 27.3415 39.4262 20.3127 40.2769 11.6563C40.3623 10.8177 39.755 10.0708 38.9321 9.98368C38.1092 9.8966 37.3762 10.5154 37.2907 11.354C36.3339 21.1401 40.9028 29.3392 50.853 35.7344C51.2584 35.9927 51.75 36.0218 52.1711 35.8602ZM43.1459 56.1688C43.567 56.0072 43.9141 55.6453 44.0609 55.1752C44.2962 54.3662 43.8433 53.5165 43.0494 53.2767C31.7748 49.8615 22.7173 51.7803 16.1214 58.972C15.5593 59.5906 15.5863 60.5602 16.1934 61.133C16.8005 61.7059 17.7485 61.6687 18.3141 61.0596C24.1375 54.6979 31.9514 53.1148 42.1885 56.2095C42.521 56.2997 42.8558 56.2801 43.1459 56.1688Z" + fill="#E2017B" + ></path> + <path + d="M44.4201 44.5516C44.7757 44.4151 45.0878 44.132 45.257 43.7513C45.3394 43.5687 45.3855 43.3713 45.3925 43.1705C45.3995 42.9697 45.3673 42.7694 45.2978 42.5814C45.2283 42.3933 45.1229 42.2212 44.9876 42.075C44.8524 41.9287 44.69 41.8113 44.5098 41.7296L34.1908 36.9676C33.4307 36.6169 32.5416 36.958 32.2068 37.729C31.8719 38.4999 32.1974 39.4095 32.954 39.7507L43.273 44.5126C43.6466 44.6851 44.0551 44.6916 44.4201 44.5516Z" + fill="#FED056" + ></path> + <path + d="M221.346 173.386C224.104 172.611 225.722 169.704 224.961 166.894C224.199 164.083 221.347 162.434 218.589 163.21C215.831 163.986 214.213 166.893 214.974 169.703C215.735 172.513 218.588 174.162 221.346 173.386Z" + fill="#2AAD52" + ></path> + <path + d="M231.852 145.038C230.666 145.372 229.4 145.295 228.349 145.242C226.334 145.121 225.392 145.164 224.81 146.079C224.358 146.787 223.427 146.986 222.734 146.536C222.039 146.076 221.834 145.129 222.286 144.421C223.88 141.921 226.558 142.077 228.518 142.192C230.532 142.312 231.475 142.269 232.057 141.355C232.705 140.337 232.269 139.149 231.506 137.292C230.687 135.302 229.662 132.821 231.203 130.389C232.798 127.89 235.475 128.046 237.435 128.16C239.45 128.281 240.392 128.238 240.974 127.323C241.426 126.615 242.357 126.416 243.05 126.866C243.745 127.326 243.94 128.275 243.498 128.981C241.913 131.478 239.226 131.325 237.275 131.208C235.261 131.087 234.319 131.13 233.736 132.045C233.089 133.062 233.525 134.25 234.287 136.108C235.106 138.097 236.131 140.578 234.591 143.01C233.835 144.163 232.874 144.751 231.852 145.038Z" + fill="#E2017B" + ></path> + <path + d="M236.088 102.696C238.949 102.696 241.268 100.333 241.268 97.4178C241.268 94.5026 238.949 92.1393 236.088 92.1393C233.227 92.1393 230.908 94.5026 230.908 97.4178C230.908 100.333 233.227 102.696 236.088 102.696Z" + fill="#E2017B" + ></path> + <defs> + <filter + id="filter0_d_433_5816" + x="114.242" + y="60.842" + width="46.332" + height="63.3339" + filterUnits="userSpaceOnUse" + color-interpolation-filters="sRGB" + > + <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood> + <feColorMatrix + in="SourceAlpha" + type="matrix" + values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" + result="hardAlpha" + ></feColorMatrix> + <feOffset dx="1" dy="1"></feOffset> + <feComposite in2="hardAlpha" operator="out"></feComposite> + <feColorMatrix + type="matrix" + values="0 0 0 0 0.988235 0 0 0 0 0.6 0 0 0 0 0.141176 0 0 0 1 0" + ></feColorMatrix> + <feBlend + mode="normal" + in2="BackgroundImageFix" + result="effect1_dropShadow_433_5816" + ></feBlend> + <feBlend + mode="normal" + in="SourceGraphic" + in2="effect1_dropShadow_433_5816" + result="shape" + ></feBlend> + </filter> + <clipPath id="clip0_433_5816"> + <rect + width="148" + height="148" + fill="white" + transform="translate(62.9082 18.0089)" + ></rect> + </clipPath> + </defs> + </svg> + } +} diff --git a/ssr/src/component/token_logo_sanitize.rs b/ssr/src/component/token_logo_sanitize.rs new file mode 100644 index 00000000..91d24985 --- /dev/null +++ b/ssr/src/component/token_logo_sanitize.rs @@ -0,0 +1,79 @@ +use leptos::*; +use leptos_use::use_event_listener; + +use crate::utils::web::FileWithUrl; + +#[component] +pub fn TokenLogoSanitize( + #[prop(into)] img_file: Signal<Option<FileWithUrl>>, + #[prop(into)] output_b64: SignalSetter<Option<String>>, +) -> impl IntoView { + let img_ref: NodeRef<html::Img> = create_node_ref(); + let canvas_ref: NodeRef<html::Canvas> = create_node_ref(); + + _ = use_event_listener(img_ref, ev::load, move |_| { + #[cfg(feature = "hydrate")] + { + use wasm_bindgen::JsCast; + use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement}; + + let Some(canvas_elem) = canvas_ref.get_untracked() else { + return; + }; + let canvas: &HtmlCanvasElement = &canvas_elem; + let img_elem = img_ref.get_untracked().unwrap(); + let img: &HtmlImageElement = &img_elem; + let im_w = img.width(); + let im_h = img.height(); + + let min_dim = im_w.min(im_h); + let canvas_dim = min_dim.min(200); + canvas.set_width(canvas_dim); + canvas.set_height(canvas_dim); + + let mut crop_x = 0.; + let mut crop_y = 0.; + let mut scaled_width = im_w as f64; + let mut scaled_height = im_h as f64; + + if im_w > im_h { + crop_x = (scaled_width - scaled_height) / 2.; + scaled_width = scaled_height; + } else { + crop_y = (scaled_height - scaled_width) / 2.; + scaled_height = scaled_width; + } + + let canvas_dim_f64 = canvas_dim as f64; + let ctx_raw = canvas.get_context("2d").unwrap().unwrap(); + let ctx: &CanvasRenderingContext2d = ctx_raw.dyn_ref().unwrap(); + ctx.draw_image_with_html_image_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh( + img, + crop_x, + crop_y, + scaled_width, + scaled_height, + 0.0, + 0.0, + canvas_dim_f64, + canvas_dim_f64, + ) + .unwrap(); + + let png_data = canvas.to_data_url_with_type("image/png").unwrap(); + + output_b64.set(Some(png_data)); + } + }); + + view! { + <canvas _ref=canvas_ref class="hidden"></canvas> + <Show when=move || img_file.with(|img| img.is_some())> + <img + _ref=img_ref + class="hidden" + src=move || img_file.with(|f| f.as_ref().unwrap().url.to_string()) + /> + </Show> + } +} diff --git a/ssr/src/consts/local.rs b/ssr/src/consts/local.rs index 783f29c1..594a0227 100644 --- a/ssr/src/consts/local.rs +++ b/ssr/src/consts/local.rs @@ -6,5 +6,5 @@ pub static METADATA_API_BASE: Lazy<Url> = pub const AGENT_URL: &str = "http://localhost:4943"; -pub const YRAL_BACKEND_CONTAINER_TAG: &str = "76bfd0fa78e4f862a4b30601f4ff3143aa974ee7"; +pub const YRAL_BACKEND_CONTAINER_TAG: &str = "692e419da7e96c9a7d0e20fe89287460df795cea"; pub const YRAL_METADATA_CONTAINER_TAG: &str = "a4879e2e711c17beeb12ed6987ba315c110be9e5"; diff --git a/ssr/src/consts/mod.rs b/ssr/src/consts/mod.rs index 0eecc80d..3d09637c 100644 --- a/ssr/src/consts/mod.rs +++ b/ssr/src/consts/mod.rs @@ -25,6 +25,7 @@ pub const NOTIFICATIONS_ENABLED_STORE: &str = "yral-notifications-enabled"; pub const NSFW_TOGGLE_STORE: &str = "nsfw-enabled"; pub const REFERRER_STORE: &str = "referrer"; pub const USER_CANISTER_ID_STORE: &str = "user-canister-id"; +pub const USER_ONBOARDING_STORE: &str = "user-onboarding"; pub static OFF_CHAIN_AGENT_GRPC_URL: Lazy<Url> = Lazy::new(|| Url::parse("https://icp-off-chain-agent.fly.dev:443").unwrap()); @@ -37,6 +38,10 @@ pub const ML_FEED_GRPC_URL: &str = "https://yral-ml-feed-server.fly.dev:443"; pub static FALLBACK_USER_INDEX: Lazy<Principal> = Lazy::new(|| Principal::from_text("rimrc-piaaa-aaaao-aaljq-cai").unwrap()); +pub const ICP_LEDGER_CANISTER_ID: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai"; + +pub const ICPUMP_LISTING_PAGE_SIZE: usize = 12; + pub mod social { pub const TELEGRAM: &str = "https://t.me/+c-LTX0Cp-ENmMzI1"; pub const DISCORD: &str = "https://discord.gg/GZ9QemnZuj"; diff --git a/ssr/src/error_template.rs b/ssr/src/error_template.rs index 12fe5115..0c5ee656 100644 --- a/ssr/src/error_template.rs +++ b/ssr/src/error_template.rs @@ -1,3 +1,4 @@ +use gloo::history::{BrowserHistory, History}; use http::status::StatusCode; use leptos::*; use thiserror::Error; @@ -36,6 +37,13 @@ pub fn ErrorTemplate( // Get Errors from Signal let errors = errors.get_untracked(); + let go_back = move || { + let history = BrowserHistory::new(); + + //go back + history.back(); + }; + // Downcast lets us take a type that implements `std::error::Error` let errors: Vec<AppError> = errors .into_iter() @@ -43,6 +51,12 @@ pub fn ErrorTemplate( .collect(); println!("Errors: {errors:#?}"); + let error_string = if !errors.is_empty() { + "It looks like our system is taking a coffee break. Try again in a bit, and we'll have it back to work!".to_string() + } else { + String::new() + }; + // Only the response code for the first error is actually sent from the server // this may be customized by the specific application #[cfg(feature = "ssr")] @@ -54,23 +68,18 @@ pub fn ErrorTemplate( } view! { - <div class="bg-black text-white"> - <h1>{if errors.len() > 1 { "Errors" } else { "Error" }}</h1> - <For - // a function that returns the items we're iterating over; a signal is fine - each=move || { errors.clone().into_iter().enumerate() } - // a unique key for each item as a reference - key=|(index, _error)| *index - // renders each item to a view - children=move |error| { - let error_string = error.1.to_string(); - let error_code = error.1.status_code(); - view! { - <h2>{error_code.to_string()}</h2> - <p>"Error: " {error_string}</p> - } - } - /> + <div class="flex flex-col w-dvw h-dvh bg-black justify-center items-center"> + <img src="/img/error-logo.svg"/> + <h1 class="p-2 text-2xl md:text-3xl font-bold text-white">"oh no!"</h1> + <div class="text-center text-xs md:text-sm text-white/60 w-full md:w-2/3 lg:w-1/3 resize-none px-8 mb-4"> + {error_string.clone()} + </div> + <button + on:click=move |_| go_back() + class="bg-primary-600 rounded-full mt-6 py-4 px-12 max-w-full text-white text-lg md:text-xl" + > + Go back + </button> </div> } } diff --git a/ssr/src/init/containers.rs b/ssr/src/init/containers.rs index 6e524a90..5c554640 100644 --- a/ssr/src/init/containers.rs +++ b/ssr/src/init/containers.rs @@ -61,7 +61,7 @@ impl TestContainers { let id = Secp256k1Identity::from_private_key(sk); let cans = AdminCanisters::new(id.clone()); - let user_index = cans.user_index_with(USER_INDEX_ID).await.unwrap(); + let user_index = cans.user_index_with(USER_INDEX_ID).await; let admin_principal = id.sender().unwrap(); let admin_canister = loop { let res = user_index diff --git a/ssr/src/init/mod.rs b/ssr/src/init/mod.rs index 3ce09f25..47d8088a 100644 --- a/ssr/src/init/mod.rs +++ b/ssr/src/init/mod.rs @@ -1,7 +1,11 @@ #[cfg(feature = "local-bin")] pub mod containers; -use std::env; +use std::{ + env, + fs::OpenOptions, + io::{BufWriter, Write}, +}; use axum_extra::extract::cookie::Key; use leptos::LeptosOptions; @@ -23,9 +27,20 @@ fn init_cf() -> gob_cloudflare::CloudflareAuth { } fn init_cookie_key() -> Key { - let cookie_key_str = env::var("COOKIE_KEY").expect("`COOKIE_KEY` is required!"); - let cookie_key_raw = - hex::decode(cookie_key_str).expect("Invalid `COOKIE_KEY` (must be length 128 hex)"); + let cookie_key_raw = { + #[cfg(not(feature = "local-bin"))] + { + let cookie_key_str = env::var("COOKIE_KEY").expect("`COOKIE_KEY` is required!"); + hex::decode(cookie_key_str).expect("Invalid `COOKIE_KEY` (must be length 128 hex)") + } + #[cfg(feature = "local-bin")] + { + use rand_chacha::rand_core::{OsRng, RngCore}; + let mut cookie_key = [0u8; 64]; + OsRng.fill_bytes(&mut cookie_key); + cookie_key.to_vec() + } + }; Key::from(&cookie_key_raw) } @@ -73,12 +88,62 @@ fn init_google_oauth() -> crate::auth::core_clients::CoreClients { ) .set_redirect_uri(RedirectUrl::new(redirect_uri).unwrap()); + let client_id = + env::var("ICPUMPFUN_GOOGLE_CLIENT_ID").expect("`ICPUMPFUN_GOOGLE_CLIENT_ID` is required!"); + let client_secret = env::var("ICPUMPFUN_GOOGLE_CLIENT_SECRET") + .expect("`ICPUMPFUN_GOOGLE_CLIENT_SECRET` is required!"); + let redirect_uri = env::var("ICPUMPFUN_GOOGLE_REDIRECT_URL") + .expect("`ICPUMPFUN_GOOGLE_REDIRECT_URL` is required!"); + + let icpump_google_oauth = CoreClient::new( + ClientId::new(client_id), + Some(ClientSecret::new(client_secret)), + IssuerUrl::new(GOOGLE_ISSUER_URL.to_string()).unwrap(), + AuthUrl::new(GOOGLE_AUTH_URL.to_string()).unwrap(), + Some(TokenUrl::new(GOOGLE_TOKEN_URL.to_string()).unwrap()), + None, + // We don't validate id_tokens against Google's public keys + Default::default(), + ) + .set_redirect_uri(RedirectUrl::new(redirect_uri).unwrap()); + CoreClients { google_oauth, hotornot_google_oauth, + icpump_google_oauth, } } +#[cfg(feature = "firestore")] +async fn init_firestoredb() -> firestore::FirestoreDb { + use firestore::{FirestoreDb, FirestoreDbOptions}; + + // firestore-rs needs the service account key to be in a file + let sa_key_file = env::var("HON_GOOGLE_SERVICE_ACCOUNT").expect("HON_GOOGLE_SERVICE_ACCOUNT"); + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open("hon_google_service_account.json") + .expect("create file"); + + let mut f = BufWriter::new(file); + f.write_all(sa_key_file.as_bytes()).expect("write file"); + f.flush().expect("flush file"); + + env::set_var( + "GOOGLE_APPLICATION_CREDENTIALS", + "hon_google_service_account.json", + ); + + let options = FirestoreDbOptions::new("hot-or-not-feed-intelligence".to_string()) + .with_database_id("ic-pump-fun".to_string()); + + FirestoreDb::with_options(options) + .await + .expect("failed to create db") +} + #[cfg(feature = "ga4")] async fn init_grpc_offchain_channel() -> tonic::transport::Channel { use crate::consts::OFF_CHAIN_AGENT_GRPC_URL; @@ -191,6 +256,8 @@ impl AppStateBuilder { google_oauth_clients: init_google_oauth(), #[cfg(feature = "ga4")] grpc_offchain_channel: init_grpc_offchain_channel().await, + #[cfg(feature = "firestore")] + firestore_db: init_firestoredb().await, }; AppStateRes { diff --git a/ssr/src/main.rs b/ssr/src/main.rs index 3a79b94d..19dc72e5 100644 --- a/ssr/src/main.rs +++ b/ssr/src/main.rs @@ -32,6 +32,9 @@ pub async fn server_fn_handler( #[cfg(feature = "ga4")] provide_context(app_state.grpc_offchain_channel.clone()); + + #[cfg(feature = "firestore")] + provide_context(app_state.firestore_db.clone()); }, request, ) @@ -58,6 +61,9 @@ pub async fn leptos_routes_handler( #[cfg(feature = "ga4")] provide_context(app_state.grpc_offchain_channel.clone()); + + #[cfg(feature = "firestore")] + provide_context(app_state.firestore_db.clone()); }, App, ); diff --git a/ssr/src/page/icpump/mod.rs b/ssr/src/page/icpump/mod.rs new file mode 100644 index 00000000..2511c813 --- /dev/null +++ b/ssr/src/page/icpump/mod.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use leptos::*; + +use crate::component::spinner::FullScreenSpinner; +use crate::consts::ICPUMP_LISTING_PAGE_SIZE; +use crate::utils::token::icpump::get_paginated_token_list; +use crate::utils::token::icpump::TokenListItem; + +#[component] +pub fn TokenListing(details: TokenListItem) -> impl IntoView { + view! { + <div class="max-h-[300px] overflow-hidden h-fit p-2 flex border hover:border-white gap-2 w-full border-transparent"> + <div class="min-w-32 relative self-start"> + <img class="mr-4 w-32 h-auto" src={details.logo} alt={details.token_name.clone()}/> + </div> + <div class="gap-1 grid h-fit"> + <span class="text-sm text-gray-500 font-bold">"$" {details.token_symbol}</span> + <span class="text-sm text-gray-500">"Name: " {details.token_name}</span> + <span class="text-sm text-gray-500">{details.description}</span> + <span class="text-xs text-gray-500">{details.formatted_created_at}</span> + <span class="text-xs text-gray-500">"Created by: " {details.user_id}</span> + </div> + </div> + } +} + +#[component] +pub fn ICPumpListing() -> impl IntoView { + let page = create_rw_signal(1); + let token_list: RwSignal<Vec<TokenListItem>> = create_rw_signal(vec![]); + let end_of_list = create_rw_signal(false); + let cache = create_rw_signal(HashMap::<u64, Vec<TokenListItem>>::new()); + + let act = create_resource(page, move |page| async move { + if let Some(cached) = cache.with_untracked(|c| c.get(&page).cloned()) { + return cached.clone(); + } + + get_paginated_token_list(page as u32).await.unwrap() + }); + + view! { + + <div class="flex flex-col justify-center mt-10 mb-10"> + <Suspense fallback=FullScreenSpinner> + {move || { + let _ = act.get().map(|res| { + if res.len() < ICPUMP_LISTING_PAGE_SIZE { + end_of_list.set(true); + } + update!(move |token_list, cache| { + *token_list = res.clone(); + cache.insert(page.get_untracked(), res.clone()); + }); + + }); + + view!{ + <div class="grid grid-col-1 md:grid-cols-2 lg:grid-cols-3 text-gray-400 gap-4"> + <For + each=move || token_list.get() + key=|t| t.token_symbol.clone() + children=move |token: TokenListItem| { + view! { + <TokenListing details=token /> + } + } + /> + </div> + + <div class="flex flex-row justify-center mt-5"> + <button on:click={ + move |_| { + page.update(|page| *page -= 1); + end_of_list.set(false); + } + } + disabled=move || page.get()==1> {"[ << ]"} </button> + <span class="mx-2"> {page} </span> + <button on:click={ + move |_| { + page.update(|page| *page += 1); + } + } + disabled=move || end_of_list.get() + > {"[ >> ]"} </button> + </div> + } + }} + </Suspense> + </div> + } +} + +#[component] +pub fn ICPumpLanding() -> impl IntoView { + view! { + <div class="min-h-screen bg-black text-white overflow-y-scroll pt-5 pb-12"> + <div class="flex ml-4 space-x-2"> + <div class="text-white"> <a href="https://twitter.com/Yral_app" target="_blank"> [twitter] </a> </div> + <div class="text-white"> <a href="https://www.instagram.com/yral_app/" target="_blank"> [instagram] </a> </div> + <div class="text-white"> <a href="https://t.me/+c-LTX0Cp-ENmMzI1" target="_blank"> [telegram] </a> </div> + </div> + <div class="flex justify-center items-center"> + <div class="font-bold text-3xl hover:font-extrabold"> <a href="/token/create"> [start a new coin] </a> </div> + </div> + <ICPumpListing /> + </div> + } +} diff --git a/ssr/src/page/logout.rs b/ssr/src/page/logout.rs index 543b72ca..460c9e3d 100644 --- a/ssr/src/page/logout.rs +++ b/ssr/src/page/logout.rs @@ -45,6 +45,7 @@ pub fn Logout() -> impl IntoView { view! { <Redirect path="/menu"/> } }) }} + </Suspense> </Loading> } diff --git a/ssr/src/page/menu.rs b/ssr/src/page/menu.rs index 68f42176..9f8d1dee 100644 --- a/ssr/src/page/menu.rs +++ b/ssr/src/page/menu.rs @@ -1,4 +1,4 @@ -use crate::component::back_btn::BackButton; +use crate::component::canisters_prov::with_cans; use crate::component::canisters_prov::{AuthCansProvider, WithAuthCans}; use crate::component::content_upload::YoutubeUpload; use crate::component::modal::Modal; @@ -100,10 +100,7 @@ fn ProfileLoaded(user_details: ProfileDetails) -> impl IntoView { <span class="text-white text-ellipsis line-clamp-1 text-xl"> {user_details.display_name_or_fallback()} </span> - <a - class="text-primary-600 text-md" - href=format!("/your-profile/{}", user_details.username_or_principal()) - > + <a class="text-primary-600 text-md" href="/your-profile"> View Profile </a> </div> @@ -190,7 +187,7 @@ pub fn Menu() -> impl IntoView { Some(()) }); - let authorized_fetch = move |cans: Canisters<true>| async move { + let authorized_fetch = with_cans(move |cans: Canisters<true>| async move { let user_principal = cans.user_principal(); match is_authorized_to_seed_content.0.get_untracked() { Some((auth, principal)) if principal == user_principal => return auth, @@ -203,11 +200,13 @@ pub fn Menu() -> impl IntoView { .check_if_authorized(user_principal) .await .unwrap_or_default() - }; + }); view! { <WithAuthCans with=authorized_fetch let:authorized> - {is_authorized_to_seed_content.0.set(Some((authorized.1, authorized.0.user_principal())))} + {is_authorized_to_seed_content + .0 + .set(Some((authorized.1, authorized.0.user_principal())))} </WithAuthCans> <Modal show=show_content_modal> <AuthCansProvider fallback=Spinner let:canisters> @@ -220,10 +219,8 @@ pub fn Menu() -> impl IntoView { <div class="min-h-screen w-full flex flex-col text-white pt-2 pb-12 bg-black items-center divide-y divide-white/10"> <div class="flex flex-col items-center w-full gap-20 pb-16"> <Title justify_center=false> - <div class="flex flex-row justify-between"> - <BackButton fallback="/".to_string()/> + <div class="flex flex-row justify-center"> <span class="font-bold text-2xl">Menu</span> - <div></div> </div>
@@ -238,7 +235,10 @@ pub fn Menu() -> impl IntoView { {r#"Your Yral account has been setup. Login with Google to not lose progress."#}
- +
+
} } diff --git a/ssr/src/page/post_view/bet.rs b/ssr/src/page/post_view/bet.rs index 5585f875..17ea1ecd 100644 --- a/ssr/src/page/post_view/bet.rs +++ b/ssr/src/page/post_view/bet.rs @@ -5,17 +5,18 @@ use leptos_use::use_interval_fn; use web_time::Duration; use crate::{ - canister::individual_user_template::{BettingStatus, PlaceBetArg, Result1}, + canister::individual_user_template::{BettingStatus, PlaceBetArg, Result3}, component::{ bullet_loader::BulletLoader, canisters_prov::AuthCansProvider, hn_icons::*, spinner::SpinnerFit, }, + page::post_view::BetEligiblePostCtx, state::canisters::{unauth_canisters, Canisters}, try_or_redirect_opt, utils::{ posts::PostDetails, profile::{BetDetails, BetKind, BetOutcome}, - timestamp::to_hh_mm_ss, + time::to_hh_mm_ss, MockPartialEq, }, }; @@ -62,7 +63,7 @@ async fn bet_on_post( post_id: u64, post_canister_id: Principal, ) -> Result { - let user = canisters.authenticated_user().await?; + let user = canisters.authenticated_user().await; let place_bet_arg = PlaceBetArg { bet_amount, @@ -74,8 +75,8 @@ async fn bet_on_post( let res = user.bet_on_currently_viewing_post(place_bet_arg).await?; let betting_status = match res { - Result1::Ok(p) => p, - Result1::Err(_e) => { + Result3::Ok(p) => p, + Result3::Err(_e) => { // todo send event that betting failed return Err(ServerFnError::new( "bet on bet_on_currently_viewing_post error".to_string(), @@ -166,6 +167,16 @@ fn HNButtonOverlay( }); let running = place_bet_action.pending(); + let BetEligiblePostCtx { can_place_bet } = expect_context(); + + create_effect(move |_| { + if !running.get() { + can_place_bet.set(true) + } else { + can_place_bet.set(false) + } + }); + view! { { @@ -196,7 +207,7 @@ fn HNButtonOverlay( coin /> - + // Bottom row: Hot Not // most of the CSS is for alignment with above icons @@ -219,10 +230,10 @@ fn HNButtonOverlay( #[component] fn WinBadge() -> impl IntoView { view! { - } @@ -266,8 +277,8 @@ fn HNWonLost(participation: BetDetails) -> impl IntoView { //
// -
-

You staked {bet_amount}tokens on {if is_hot { "Hot" } else { "Not" }}.

+
+

You staked {bet_amount} tokens on {if is_hot { "Hot" } else { "Not" }} .

{if let Some(reward) = participation.reward() { format!("You received {reward} tokens.") @@ -281,6 +292,7 @@ fn HNWonLost(participation: BetDetails) -> impl IntoView { } else { view! { } }} +

@@ -357,8 +369,8 @@ fn HNAwaitingResults(
-

- You staked {bet_amount}tokens on {bet_direction_text}Result is still pending +

+ You staked {bet_amount} tokens on {bet_direction_text} Result is still pending

} @@ -373,14 +385,14 @@ pub fn HNUserParticipation( view! { {match participation.outcome { BetOutcome::AwaitingResult => { - view! { } + view! { } } BetOutcome::Won(_) => { - view! { } + view! { } } BetOutcome::Draw(_) => view! { "Draw" }.into_view(), BetOutcome::Lost => { - view! { } + view! { } } } .into_view()} @@ -396,13 +408,13 @@ fn MaybeHNButtons( refetch_bet: Trigger, ) -> impl IntoView { let post = store_value(post); - let is_betting_enabled = create_resource( + let is_betting_enabled: Resource<(), Option> = create_resource( move || (), move |_| { let post = post.get_value(); async move { let canisters = unauth_canisters(); - let user = canisters.individual_user(post.canister_id).await.ok()?; + let user = canisters.individual_user(post.canister_id).await; let res = user .get_hot_or_not_bet_details_for_this_post(post.post_id) .await @@ -411,6 +423,7 @@ fn MaybeHNButtons( } }, ); + let BetEligiblePostCtx { can_place_bet } = expect_context(); view! { @@ -418,6 +431,7 @@ fn MaybeHNButtons( is_betting_enabled() .and_then(|enabled| { if !enabled.unwrap_or_default() { + can_place_bet.set(false); return None; } Some( @@ -450,7 +464,7 @@ fn ShadowBg() -> impl IntoView {
+ >
} } @@ -475,7 +489,7 @@ pub fn HNGameOverlay(post: PostDetails) -> impl IntoView { let cans = canisters.clone(); async move { let post = post.get_value(); - let user = cans.authenticated_user().await?; + let user = cans.authenticated_user().await; let bet_participation = user .get_individual_hot_or_not_bet_placed_by_this_profile( post.canister_id, @@ -501,11 +515,11 @@ pub fn HNGameOverlay(post: PostDetails) -> impl IntoView { Some( if let Some(participation) = participation { view! { - + } } else { view! { - + } }, ) diff --git a/ssr/src/page/post_view/mod.rs b/ssr/src/page/post_view/mod.rs index dc2823d1..af1c23f3 100644 --- a/ssr/src/page/post_view/mod.rs +++ b/ssr/src/page/post_view/mod.rs @@ -4,10 +4,13 @@ pub mod overlay; pub mod single_post; pub mod video_iter; pub mod video_loader; +use priority_queue::DoublePriorityQueue; +use std::cmp::Reverse; + use crate::{ component::{scrolling_post_view::ScrollingPostView, spinner::FullScreenSpinner}, consts::NSFW_TOGGLE_STORE, - state::canisters::{unauth_canisters, Canisters}, + state::canisters::{authenticated_canisters, unauth_canisters, Canisters}, try_or_redirect, utils::{ posts::{get_post_uid, FetchCursor, PostDetails}, @@ -29,6 +32,12 @@ struct PostParams { post_id: u64, } +#[derive(Clone, Default)] +pub struct BetEligiblePostCtx { + // This is true if betting is enabled for the current post and no bet has been placed + pub can_place_bet: RwSignal, +} + #[derive(Clone, Default)] pub struct PostViewCtx { fetch_cursor: RwSignal, @@ -39,6 +48,8 @@ pub struct PostViewCtx { video_queue: RwSignal>, current_idx: RwSignal, queue_end: RwSignal, + priority_q: RwSignal)>>, // we are using DoublePriorityQueue for GC in the future through pop_min + batch_cnt: RwSignal, } #[component] @@ -52,6 +63,7 @@ pub fn CommonPostViewWithUpdates( video_queue, current_idx, queue_end, + .. } = expect_context(); let recovering_state = create_rw_signal(false); @@ -176,13 +188,7 @@ pub fn PostViewWithUpdates(initial_post: Option) -> impl IntoView { fetch_cursor.try_update(|c| c.advance()); }); - view! { - - } + view! { } } #[component] @@ -191,67 +197,85 @@ pub fn PostViewWithUpdatesMLFeed(initial_post: Option) -> impl Into fetch_cursor, video_queue, queue_end, + priority_q, + batch_cnt, .. } = expect_context(); let (nsfw_enabled, _, _) = use_local_storage::(NSFW_TOGGLE_STORE); - let auth_canisters: RwSignal>> = expect_context(); - let fetch_video_action = create_action(move |_| async move { - loop { - let Some(mut cursor) = fetch_cursor.try_get_untracked() else { - return; - }; - let Some(auth_canisters) = auth_canisters.try_get_untracked() else { - return; - }; - let Some(nsfw_enabled) = nsfw_enabled.try_get_untracked() else { - return; - }; - let unauth_canisters = unauth_canisters(); + let auth_cans = authenticated_canisters(); - let chunks = if let Some(canisters) = auth_canisters.as_ref() { - let mut fetch_stream = VideoFetchStream::new(canisters, cursor); - fetch_stream + let fetch_video_action = create_action(move |_| { + let auth_cans = auth_cans.clone(); + async move { + while priority_q.with_untracked(|q| q.len()) < 15 { + let Some(cursor) = fetch_cursor.try_get_untracked() else { + return; + }; + let Some(nsfw_enabled) = nsfw_enabled.try_get_untracked() else { + return; + }; + let Some(batch_cnt_val) = batch_cnt.try_get_untracked() else { + return; + }; + + let canisters = auth_cans.wait_untracked().await; + let cans_true = canisters.unwrap().canisters().unwrap(); + + let mut fetch_stream = VideoFetchStream::new(&cans_true, cursor); + let chunks = fetch_stream .fetch_post_uids_hybrid(3, nsfw_enabled, video_queue.get_untracked()) - .await - } else { - cursor.set_limit(15); - let fetch_stream = VideoFetchStream::new(&unauth_canisters, cursor); - fetch_stream.fetch_post_uids_chunked(3, nsfw_enabled).await - }; + .await; - let res = try_or_redirect!(chunks); - let mut chunks = res.posts_stream; - let mut cnt = 0; - while let Some(chunk) = chunks.next().await { - cnt += chunk.len(); - video_queue.try_update(|q| { - for uid in chunk { - let uid = try_or_redirect!(uid); - q.push(uid); - } - }); - } - leptos::logging::log!("feed type: {:?}", res.res_type); - if res.res_type == FeedResultType::PostCache { - fetch_cursor.try_update(|c| c.advance_and_set_limit(30)); - } + let res = try_or_redirect!(chunks); + let mut chunks = res.posts_stream; + let cnt = create_rw_signal(0); + while let Some(chunk) = chunks.next().await { + update!(move |video_queue, priority_q, cnt| { + for uid in chunk { + let post_detail = try_or_redirect!(uid); - if res.end || cnt >= 8 { - queue_end.try_set(res.end); - break; + if video_queue.len() < 10 { + video_queue.push(post_detail); + } else { + priority_q.push(post_detail, (batch_cnt_val, Reverse(*cnt))); + } + + *cnt += 1; + } + }); + } + + leptos::logging::log!("feed type: {:?}", res.res_type); + if res.res_type != FeedResultType::MLFeed { + fetch_cursor.try_update(|c| { + c.set_limit(15); + c.advance_and_set_limit(15) + }); + } + + if res.end { + queue_end.try_set(res.end); + } + + batch_cnt.update(|x| *x += 1); } + + update!(move |video_queue, priority_q| { + let mut cnt = 0; + while let Some((next, _)) = priority_q.pop_max() { + video_queue.push(next); + cnt += 1; + if cnt >= 10 { + break; + } + } + }); } }); - view! { - - } + view! { } } #[component] @@ -302,16 +326,33 @@ pub fn PostView() -> impl IntoView { view! { - { - {move || { - fetch_first_video_uid() - .and_then(|initial_post| { - let initial_post = initial_post.ok()?; - Some(view! { }) - }) + + {{ + move || { + fetch_first_video_uid() + .and_then(|initial_post| { + let initial_post = initial_post.ok()?; + #[cfg(any(feature = "local-bin", feature = "local-lib"))] + { Some(view! { }) } + #[cfg(not(any(feature = "local-bin", feature = "local-lib")))] + { Some(view! { }) } + }) + } }} - } } } + +// #[component] +// pub fn PostView() -> impl IntoView { +// if show_cdao_page() { +// view! { +// +// } +// } else { +// view! { +// +// } +// } +// } diff --git a/ssr/src/page/post_view/overlay.rs b/ssr/src/page/post_view/overlay.rs index 9a7e037d..f29cfd79 100644 --- a/ssr/src/page/post_view/overlay.rs +++ b/ssr/src/page/post_view/overlay.rs @@ -1,6 +1,8 @@ use crate::{ component::{ - canisters_prov::WithAuthCans, hn_icons::HomeFeedShareIcon, modal::Modal, + canisters_prov::{with_cans, WithAuthCans}, + hn_icons::HomeFeedShareIcon, + modal::Modal, option::SelectOption, }, state::canisters::{auth_canisters_store, Canisters}, @@ -58,9 +60,7 @@ fn LikeAndAuthCanLoader(post: PostDetails) -> impl IntoView { LikeVideo.send_event(post_details, likes, canister_store); } }); - let Ok(individual) = canisters.individual_user(post_canister).await else { - return; - }; + let individual = canisters.individual_user(post_canister).await; match individual .update_post_toggle_like_status_by_caller(post_id) .await @@ -74,7 +74,7 @@ fn LikeAndAuthCanLoader(post: PostDetails) -> impl IntoView { } }); - let liked_fetch = move |cans: Canisters| async move { + let liked_fetch = with_cans(move |cans: Canisters| async move { if let Some(liked) = initial_liked.0 { return (liked, initial_liked.1); } @@ -86,7 +86,7 @@ fn LikeAndAuthCanLoader(post: PostDetails) -> impl IntoView { (false, likes.try_get_untracked().unwrap_or_default()) } } - }; + }); let liking = like_toggle.pending(); @@ -107,7 +107,6 @@ fn LikeAndAuthCanLoader(post: PostDetails) -> impl IntoView { - } } @@ -182,34 +181,62 @@ pub fn VideoDetailsOverlay(post: PostDetails) -> impl IntoView { }); view! { -
-
-
- - - -
-
-
- - - {post.display_name} - - - "|" - - - {post.views} - -
- +
+
+
+ + + +
+
+
+ + {post.display_name} + + "|" + + + {post.views} +
+
+
+
+ + + + + + +
+
+ +
+
+
+ +
+ Share +
+

+ {video_url} +

+ +
+
diff --git a/ssr/src/page/post_view/single_post.rs b/ssr/src/page/post_view/single_post.rs index dccae08f..7b98b3f8 100644 --- a/ssr/src/page/post_view/single_post.rs +++ b/ssr/src/page/post_view/single_post.rs @@ -46,13 +46,9 @@ fn SinglePostViewInner(post: PostDetails) -> impl IntoView { class="absolute top-0 left-0 bg-cover bg-center w-full h-full z-[1] blur-lg" style:background-color="rgb(0, 0, 0)" style:background-image=format!("url({bg_url})") - /> + >
- +
@@ -63,10 +59,11 @@ fn SinglePostViewInner(post: PostDetails) -> impl IntoView { fn UnavailablePost() -> impl IntoView { view! {
- - Post is unavailable - -
@@ -92,20 +89,18 @@ pub fn SinglePost() -> impl IntoView { view! { - {move || fetch_post().map(|post| match post { - Ok(post) => view! { - - }, - Err(PostFetchError::Invalid) => view! { - - }, - Err(PostFetchError::Unavailable) => view! { - - }, - Err(PostFetchError::GetUid(e)) => view! { - - } - })} + {move || { + fetch_post() + .map(|post| match post { + Ok(post) => view! { }, + Err(PostFetchError::Invalid) => view! { }, + Err(PostFetchError::Unavailable) => view! { }, + Err(PostFetchError::GetUid(e)) => { + view! { } + } + }) + }} + } } diff --git a/ssr/src/page/post_view/video_iter.rs b/ssr/src/page/post_view/video_iter.rs index 1ef26cda..0a85c064 100644 --- a/ssr/src/page/post_view/video_iter.rs +++ b/ssr/src/page/post_view/video_iter.rs @@ -18,7 +18,7 @@ pub async fn post_liked_by_me( post_canister: Principal, post_id: u64, ) -> Result<(bool, u64), PostViewError> { - let individual = canisters.individual_user(post_canister).await?; + let individual = canisters.individual_user(post_canister).await; let post = individual .get_individual_post_details_by_id(post_id) .await?; @@ -30,6 +30,7 @@ type PostsStream<'a> = Pin VideoFetchStream<'a, AUTH> { chunks: usize, allow_nsfw: bool, ) -> Result, PostViewError> { - let post_cache = self.canisters.post_cache().await?; + let post_cache = self.canisters.post_cache().await; let top_posts_fut = post_cache .get_top_posts_aggregated_from_canisters_on_this_network_for_home_feed_cursor( self.cursor.start, @@ -107,7 +108,6 @@ impl<'a, const AUTH: bool> VideoFetchStream<'a, AUTH> { #[cfg(feature = "hydrate")] { use crate::utils::ml_feed::ml_feed_grpcweb::MLFeed; - use leptos::expect_context; let ml_feed: MLFeed = expect_context(); @@ -168,6 +168,39 @@ impl<'a, const AUTH: bool> VideoFetchStream<'a, AUTH> { }); } } +} + +impl<'a> VideoFetchStream<'a, true> { + pub async fn fetch_post_uids_mlfeed_cache_chunked( + &self, + chunks: usize, + allow_nsfw: bool, + ) -> Result, PostViewError> { + let cans_true = self.canisters; + + let user_canister = cans_true.authenticated_user().await; + let top_posts_fut = + user_canister.get_ml_feed_cache_paginated(self.cursor.start, self.cursor.limit); + + let top_posts = top_posts_fut.await?; + if top_posts.is_empty() { + return self.fetch_post_uids_chunked(chunks, allow_nsfw).await; + } + + let end = false; + let chunk_stream = top_posts + .into_iter() + .map(move |item| get_post_uid(self.canisters, item.canister_id, item.post_id)) + .collect::>() + .filter_map(|res| async { res.transpose() }) + .chunks(chunks); + + Ok(FetchVideosRes { + posts_stream: Box::pin(chunk_stream), + end, + res_type: FeedResultType::MLFeedCache, + }) + } pub async fn fetch_post_uids_hybrid( &mut self, @@ -175,13 +208,10 @@ impl<'a, const AUTH: bool> VideoFetchStream<'a, AUTH> { _allow_nsfw: bool, video_queue: Vec, ) -> Result, PostViewError> { - // If video_queue len is < 10, fetch from fetch_post_uids_chunked - // else fetch from fetch_post_uids_ml_feed_chunked - // if that fails fallback to fetch_post_uids_chunked - if video_queue.len() < 10 { self.cursor.set_limit(15); - self.fetch_post_uids_chunked(chunks, _allow_nsfw).await + self.fetch_post_uids_mlfeed_cache_chunked(chunks, _allow_nsfw) + .await } else { let res = self .fetch_post_uids_ml_feed_chunked(chunks, _allow_nsfw, video_queue) @@ -191,7 +221,8 @@ impl<'a, const AUTH: bool> VideoFetchStream<'a, AUTH> { Ok(res) => Ok(res), Err(_) => { self.cursor.set_limit(15); - self.fetch_post_uids_chunked(chunks, _allow_nsfw).await + self.fetch_post_uids_mlfeed_cache_chunked(chunks, _allow_nsfw) + .await } } } diff --git a/ssr/src/page/post_view/video_loader.rs b/ssr/src/page/post_view/video_loader.rs index f53d16d4..c860887f 100644 --- a/ssr/src/page/post_view/video_loader.rs +++ b/ssr/src/page/post_view/video_loader.rs @@ -1,15 +1,21 @@ use std::cmp::Ordering; +use codee::string::FromToStringCodec; use leptos::{html::Video, *}; +use leptos_use::storage::use_local_storage; use leptos_use::use_event_listener; +use crate::consts::USER_ONBOARDING_STORE; +use crate::page::post_view::BetEligiblePostCtx; use crate::utils::event_streaming::events::VideoWatched; use crate::{ canister::{ individual_user_template::PostViewDetailsFromFrontend, utils::{bg_url, mp4_url}, }, - component::{feed_popup::FeedPopUp, video_player::VideoPlayer}, + component::{ + feed_popup::FeedPopUp, onboarding_flow::OnboardingPopUp, video_player::VideoPlayer, + }, state::{ auth::account_connected_reader, canisters::unauth_canisters, local_storage::use_referrer_store, @@ -34,6 +40,13 @@ pub fn BgView( let (show_refer_login_popup, set_show_refer_login_popup) = create_signal(true); let (referrer_store, _, _) = use_referrer_store(); + let onboarding_eligible_post_context = BetEligiblePostCtx::default(); + provide_context(onboarding_eligible_post_context.clone()); + + let (show_onboarding_popup, set_show_onboarding_popup) = create_signal(false); + let (is_onboarded, set_onboarded, _) = + use_local_storage::(USER_ONBOARDING_STORE); + create_effect(move |_| { if current_idx.get() % 5 != 0 { set_show_login_popup.update(|n| *n = false); @@ -43,6 +56,14 @@ pub fn BgView( Some(()) }); + create_effect(move |_| { + if onboarding_eligible_post_context.can_place_bet.get() && (!is_onboarded.get()) { + set_show_onboarding_popup.update(|show| *show = true); + } else { + set_show_onboarding_popup.update(|show| *show = false); + } + }); + view! {
- {move || post().map(|post| view! { })} + + + + {move || post().map(|post| view! { })} {children()}
} @@ -140,7 +164,6 @@ pub fn VideoView( let send_view_res = canisters .individual_user(canister_id) .await - .ok()? .update_post_add_view_details(post_id, payload) .await; @@ -223,11 +246,5 @@ pub fn VideoViewForQueue( let post = Signal::derive(move || video_queue.with(|q| q.get(idx).cloned())); - view! { - - } + view! { } } diff --git a/ssr/src/page/profile/mod.rs b/ssr/src/page/profile/mod.rs index 87bceef8..f6aeee68 100644 --- a/ssr/src/page/profile/mod.rs +++ b/ssr/src/page/profile/mod.rs @@ -4,6 +4,7 @@ mod posts; mod profile_iter; pub mod profile_post; mod speculation; +mod tokens; use candid::Principal; use leptos::*; @@ -11,13 +12,16 @@ use leptos_icons::*; use leptos_router::*; use crate::{ - component::{back_btn::BackButton, connect::ConnectLogin, spinner::FullScreenSpinner}, + component::{ + canisters_prov::AuthCansProvider, connect::ConnectLogin, spinner::FullScreenSpinner, + }, state::{auth::account_connected_reader, canisters::unauth_canisters}, utils::{posts::PostDetails, profile::ProfileDetails}, }; use posts::ProfilePosts; use speculation::ProfileSpeculations; +use tokens::ProfileTokens; #[derive(Clone, Default)] pub struct ProfilePostsContext { @@ -43,12 +47,13 @@ fn Stat(stat: u64, #[prop(into)] info: String) -> impl IntoView { } #[component] -fn ListSwitcher(user_canister: Principal) -> impl IntoView { +fn ListSwitcher(user_canister: Principal, user_principal: Principal) -> impl IntoView { let (cur_tab, set_cur_tab) = create_query_signal::("tab"); let current_tab = create_memo(move |_| { with!(|cur_tab| match cur_tab.as_deref() { Some("posts") => 0, Some("speculations") => 1, + Some("tokens") => 2, _ => 0, }) }); @@ -61,23 +66,29 @@ fn ListSwitcher(user_canister: Principal) -> impl IntoView { }; view! { -
+
+
- } - > - + + + + + + + +
} @@ -93,9 +104,6 @@ fn ProfileViewInner(user: ProfileDetails, user_canister: Principal) -> impl Into view! {
-
- -
@@ -116,11 +124,11 @@ fn ProfileViewInner(user: ProfileDetails, user_canister: Principal) -> impl Into
// TODO: Add username when it's available //

@ {username_or_principal}

-

{earnings} Earnings

+

{earnings}Earnings

- +
@@ -129,10 +137,10 @@ fn ProfileViewInner(user: ProfileDetails, user_canister: Principal) -> impl Into
// // - - + +
- +
} @@ -149,17 +157,50 @@ pub fn ProfileView() -> impl IntoView { }) }; - let user_details = create_resource(principal, |principal| async move { - let canisters = unauth_canisters(); - let user_canister = canisters - .get_individual_canister_by_user_principal(principal?) - .await - .ok()??; - let user = canisters.individual_user(user_canister).await.ok()?; - let user_details = user.get_profile_details().await.ok()?; - Some((user_details.into(), user_canister)) - }); + let user_details = create_resource( + || {}, + move |_| async move { + let canisters = unauth_canisters(); + + let user_canister = canisters + .get_individual_canister_by_user_principal(principal()?) + .await + .ok()??; + let user = canisters.individual_user(user_canister).await; + let user_details = user.get_profile_details().await.ok()?; + Some((user_details.into(), user_canister)) + }, + ); + + view! { + + {move || { + user_details + .get() + .map(|user_details| { + view! { } + }) + }} + + + } +} + +#[component] +pub fn YourProfileView() -> impl IntoView { + view! { + + + + } +} + +#[component] +pub fn ProfileComponent(user_details: Option<(ProfileDetails, Principal)>) -> impl IntoView { let ProfilePostsContext { video_queue, start_index, @@ -174,23 +215,12 @@ pub fn ProfileView() -> impl IntoView { }); view! { - - {move || { - user_details - .get() - .map(|user| { - view! { - {move || { - if let Some((user, user_canister)) = user.clone() { - view! { } - } else { - view! { } - } - }} - } - }) - }} - - + {move || { + if let Some((user, user_canister)) = user_details.clone() { + view! { } + } else { + view! { } + } + }} } } diff --git a/ssr/src/page/profile/profile_iter.rs b/ssr/src/page/profile/profile_iter.rs index 1dc60df8..fa51ffbd 100644 --- a/ssr/src/page/profile/profile_iter.rs +++ b/ssr/src/page/profile/profile_iter.rs @@ -2,7 +2,7 @@ use candid::Principal; use futures::stream::{FuturesOrdered, StreamExt, TryStreamExt}; use crate::{ - canister::individual_user_template::{GetPostsOfUserProfileError, Result5}, + canister::individual_user_template::{GetPostsOfUserProfileError, Result11}, state::canisters::Canisters, utils::posts::{get_post_uid, PostDetails, PostViewError}, }; @@ -41,7 +41,7 @@ impl ProfVideoStream<10> for ProfileVideoBetsStream { canisters: &Canisters, user_canister: Principal, ) -> Result { - let user = canisters.individual_user(user_canister).await?; + let user = canisters.individual_user(user_canister).await; let bets = user .get_hot_or_not_bets_placed_by_this_profile_with_pagination(cursor.start) .await?; @@ -65,12 +65,12 @@ impl ProfVideoStream for ProfileVideoStream { canisters: &Canisters, user_canister: Principal, ) -> Result { - let user = canisters.individual_user(user_canister).await?; + let user = canisters.individual_user(user_canister).await; let posts = user .get_posts_of_this_user_profile_with_pagination_cursor(cursor.start, cursor.limit) .await?; match posts { - Result5::Ok(v) => { + Result11::Ok(v) => { let end = v.len() < LIMIT as usize; let posts = v .into_iter() @@ -78,7 +78,7 @@ impl ProfVideoStream for ProfileVideoStream { .collect::>(); Ok(PostsRes { posts, end }) } - Result5::Err(GetPostsOfUserProfileError::ReachedEndOfItemsList) => Ok(PostsRes { + Result11::Err(GetPostsOfUserProfileError::ReachedEndOfItemsList) => Ok(PostsRes { posts: vec![], end: true, }), diff --git a/ssr/src/page/profile/speculation.rs b/ssr/src/page/profile/speculation.rs index 0d8021fd..53b64876 100644 --- a/ssr/src/page/profile/speculation.rs +++ b/ssr/src/page/profile/speculation.rs @@ -13,7 +13,7 @@ use crate::{ utils::{ posts::PostDetails, profile::{BetDetails, BetOutcome, BetsProvider, ProfileDetails}, - timestamp::to_hh_mm_ss, + time::to_hh_mm_ss, }, }; @@ -31,14 +31,9 @@ pub fn ExternalUser(user: Option) -> impl IntoView { view! {
- -
-
- {name} +
+
{name}
} } @@ -95,7 +90,10 @@ fn BetTimer(post: PostDetails, details: BetDetails) -> impl IntoView { view! {
-
+
{move || to_hh_mm_ss(time_remaining())}
@@ -114,7 +112,7 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi move || (bet_canister, details.post_id), move |(canister_id, post_id)| async move { let canister = unauth_canisters(); - let user = canister.individual_user(canister_id).await.ok()?; + let user = canister.individual_user(canister_id).await; let post_details = user.get_individual_post_details_by_id(post_id).await.ok()?; Some(PostDetails::from_canister_post( false, @@ -128,7 +126,7 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi move || bet_canister, move |canister_id| async move { let canister = unauth_canisters(); - let user = canister.individual_user(canister_id).await.ok()?; + let user = canister.individual_user(canister_id).await; let profile_details = user.get_profile_details().await.ok()?; Some(ProfileDetails::from(profile_details)) }, @@ -141,7 +139,7 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi amt, view! {
- + You Won
}.into_view(), @@ -171,10 +169,9 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi {move || { let post = post_details().flatten()?; - Some(view! { - - }) + Some(view! { }) }} + }, ), @@ -182,7 +179,10 @@ pub fn Speculation(details: BetDetails, _ref: NodeRef) -> impl IntoVi view! { diff --git a/ssr/src/page/profile/tokens.rs b/ssr/src/page/profile/tokens.rs new file mode 100644 index 00000000..0aedba6a --- /dev/null +++ b/ssr/src/page/profile/tokens.rs @@ -0,0 +1,157 @@ +use candid::Principal; +use leptos::*; +use leptos_icons::*; + +use crate::{ + component::{ + bullet_loader::BulletLoader, canisters_prov::AuthCansProvider, + claim_tokens::ClaimTokensOrRedirectError, token_confetti_symbol::TokenConfettiSymbol, + }, + state::canisters::unauth_canisters, + utils::token::{get_token_metadata, TokenCans}, +}; + +#[component] +fn TokenViewFallback() -> impl IntoView { + view! { +
+ } +} + +#[component] +fn TokenView(user_principal: Principal, token: TokenCans) -> impl IntoView { + let token_info = create_resource( + || (), + move |_| async move { + let cans = unauth_canisters(); + let metadata = + get_token_metadata(&cans, user_principal, token.governance, token.ledger).await?; + + Ok::<_, ServerFnError>(metadata) + }, + ); + let token_link = move || format!("/token/info/{}/{}", token.root, user_principal.to_text()); + + view! { + + + {move || { + token_info() + .and_then(|info| info.ok()) + .map(|info| { + view! { +
+
+ + {info.name} +
+
+ + {format!("{} {}", info.balance.humanize(), info.symbol)} + +
+ Details +
+ +
+
+
+
+ } + }) + }} + + + } +} + +#[component] +fn CreateYourToken(header_text: &'static str) -> impl IntoView { + view! { +
+ + {header_text}
Meme Coin +
+ +
+ } +} + +#[component] +pub fn ProfileTokens(user_canister: Principal, user_principal: Principal) -> impl IntoView { + let token_list = create_resource( + || (), + move |_| async move { + let cans = unauth_canisters(); + let user = cans.individual_user(user_canister).await; + let tokens: Vec<_> = user + .deployed_cdao_canisters() + .await? + .into_iter() + .map(|cans| TokenCans { + governance: cans.governance, + ledger: cans.ledger, + root: cans.root, + }) + .collect(); + Ok::<_, ServerFnError>(tokens) + }, + ); + + view! { +
+ + +
+ } + }> + {move || { + token_list() + .map(|tokens| tokens.unwrap_or_default()) + .map(|tokens| { + let empty = tokens.is_empty(); + view! { + {tokens + .into_iter() + .map(|token| view! { }) + .collect_view()} + + + + { + let is_native_profile = canisters.user_principal() + == user_principal; + view! { + + + + + + + Create + + + } + } + + + } + }) + }} + + +
+ } +} diff --git a/ssr/src/page/refer_earn/history.rs b/ssr/src/page/refer_earn/history.rs index e4198177..e03c0d21 100644 --- a/ssr/src/page/refer_earn/history.rs +++ b/ssr/src/page/refer_earn/history.rs @@ -5,7 +5,7 @@ use crate::component::canisters_prov::AuthCansProvider; use crate::component::infinite_scroller::InfiniteScroller; use crate::{ state::canisters::Canisters, - utils::{profile::propic_from_principal, timestamp::get_day_month}, + utils::{profile::propic_from_principal, time::get_day_month}, }; use history_provider::*; @@ -112,9 +112,9 @@ mod history_provider { from: usize, end: usize, ) -> Result, AgentError> { - use crate::canister::individual_user_template::{MintEvent, Result7, TokenEvent}; + use crate::canister::individual_user_template::{MintEvent, Result15, TokenEvent}; use crate::utils::route::failure_redirect; - let individual = self.0.authenticated_user().await?; + let individual = self.0.authenticated_user().await; let history = individual .get_user_utility_token_transaction_history_with_pagination( from as u64, @@ -122,8 +122,8 @@ mod history_provider { ) .await?; let history = match history { - Result7::Ok(history) => history, - Result7::Err(_) => { + Result15::Ok(history) => history, + Result15::Err(_) => { failure_redirect("failed to get posts"); return Ok(PageEntry { data: vec![], @@ -173,7 +173,7 @@ mod history_provider { ChaCha8Rng, }; - use crate::utils::current_epoch; + use crate::utils::time::current_epoch; use super::*; diff --git a/ssr/src/page/root.rs b/ssr/src/page/root.rs index 29745054..6aed43fd 100644 --- a/ssr/src/page/root.rs +++ b/ssr/src/page/root.rs @@ -2,14 +2,14 @@ use candid::Principal; use leptos::*; use leptos_router::*; -use crate::component::spinner::FullScreenSpinner; #[cfg(feature = "ssr")] use crate::{canister::post_cache, state::canisters::unauth_canisters}; +use crate::{component::spinner::FullScreenSpinner, utils::host::show_cdao_page}; #[server] async fn get_top_post_id() -> Result, ServerFnError> { let canisters = unauth_canisters(); - let post_cache = canisters.post_cache().await?; + let post_cache = canisters.post_cache().await; let top_items = match post_cache .get_top_posts_aggregated_from_canisters_on_this_network_for_home_feed_cursor( @@ -35,6 +35,44 @@ async fn get_top_post_id() -> Result, ServerFnError> { Ok(Some((top_item.publisher_canister_id, top_item.post_id))) } +#[server] +async fn get_top_post_id_mlcache() -> Result, ServerFnError> { + use crate::auth::server_impl::extract_principal_from_cookie; + use axum_extra::extract::{cookie::Key, SignedCookieJar}; + use leptos_axum::extract_with_state; + + let key: Key = expect_context(); + let jar: SignedCookieJar = extract_with_state(&key).await?; + let principal = extract_principal_from_cookie(&jar)?; + if principal.is_none() { + return get_top_post_id().await; + } + + let canisters = unauth_canisters(); + let user_canister_id = canisters + .get_individual_canister_by_user_principal(principal.unwrap()) + .await?; + if user_canister_id.is_none() { + return get_top_post_id().await; + } + + let user_canister = canisters.individual_user(user_canister_id.unwrap()).await; + + let top_items = user_canister + .get_ml_feed_cache_paginated(0, 1) + .await + .unwrap(); + if top_items.is_empty() { + return get_top_post_id().await; + } + + let Some(top_item) = top_items.first() else { + return Ok(None); + }; + + Ok(Some((top_item.canister_id, top_item.post_id))) +} + // TODO: Use this when we shift to the new ml feed for first post // #[server] // async fn get_top_post_id_mlfeed() -> Result, ServerFnError> { @@ -61,8 +99,26 @@ async fn get_top_post_id() -> Result, ServerFnError> { // } #[component] -pub fn RootPage() -> impl IntoView { - let target_post = create_resource(|| (), |_| get_top_post_id()); +pub fn CreatorDaoRootPage() -> impl IntoView { + view! { + {move || { + let redirect_url = "/board".to_string(); + view! { } + }} + } +} + +#[component] +pub fn YralRootPage() -> impl IntoView { + let target_post; + #[cfg(any(feature = "local-bin", feature = "local-lib"))] + { + target_post = create_resource(|| (), |_| get_top_post_id()); + } + #[cfg(not(any(feature = "local-bin", feature = "local-lib")))] + { + target_post = create_resource(|| (), |_| get_top_post_id_mlcache()); + } view! { @@ -77,10 +133,19 @@ pub fn RootPage() -> impl IntoView { Ok(None) => "/error?err=No Posts Found".to_string(), Err(e) => format!("/error?err={e}"), }; - view! { } + view! { } }) }} } } + +#[component] +pub fn RootPage() -> impl IntoView { + if show_cdao_page() { + view! { } + } else { + view! { } + } +} diff --git a/ssr/src/page/settings/mod.rs b/ssr/src/page/settings/mod.rs index 08edf667..a29af5e5 100644 --- a/ssr/src/page/settings/mod.rs +++ b/ssr/src/page/settings/mod.rs @@ -90,10 +90,7 @@ fn ProfileLoaded(user_details: ProfileDetails) -> impl IntoView { {user_details.display_name_or_fallback()} - + View Profile
diff --git a/ssr/src/page/token/create/mod.rs b/ssr/src/page/token/create/mod.rs new file mode 100644 index 00000000..0d93d67f --- /dev/null +++ b/ssr/src/page/token/create/mod.rs @@ -0,0 +1,633 @@ +#[cfg(feature = "ssr")] +mod server_impl; + +use crate::{ + component::{back_btn::BackButton, title::Title, token_logo_sanitize::TokenLogoSanitize}, + state::canisters::{auth_canisters_store, authenticated_canisters, CanistersAuthWire}, + utils::{ + event_streaming::events::{ + TokenCreationCompleted, TokenCreationFailed, TokenCreationStarted, + }, + web::FileWithUrl, + }, +}; +use leptos::*; +use std::env; + +use server_fn::codec::Cbor; +use sns_validation::{humanize::parse_tokens, pbs::nns_pb::Tokens}; +use sns_validation::{ + humanize::{ + format_duration, format_percentage, format_tokens, parse_duration, parse_percentage, + }, + pbs::sns_pb::SnsInitPayload, +}; + +use super::{popups::TokenCreationPopup, sns_form::SnsFormState}; + +use icp_ledger::AccountIdentifier; + +#[server] +async fn is_server_available() -> Result<(bool, AccountIdentifier), ServerFnError> { + server_impl::is_server_available().await +} + +#[server( + input = Cbor +)] +async fn deploy_cdao_canisters( + cans_wire: CanistersAuthWire, + create_sns: SnsInitPayload, +) -> Result<(), ServerFnError> { + server_impl::deploy_cdao_canisters(cans_wire, create_sns).await +} + +#[component] +fn TokenImage() -> impl IntoView { + let ctx = expect_context::(); + let img_file = create_rw_signal(None::); + let fstate = ctx.form_state; + + // let img_file = create_rw_signal(None::); + let (logo_b64, set_logo_b64) = slice!(fstate.logo_b64); + + let on_file_input = move |ev: ev::Event| { + _ = ev.target().and_then(|_target| { + #[cfg(feature = "hydrate")] + { + use wasm_bindgen::JsCast; + use web_sys::HtmlInputElement; + + let input = _target.dyn_ref::()?; + let file = input.files()?.get(0)?; + + img_file.set(Some(FileWithUrl::new(file.into()))); + } + Some(()) + }) + }; + + let file_input_ref: NodeRef = create_node_ref::(); + + let on_edit_click = move |_| { + // Trigger the file input click + if let Some(input) = file_input_ref.get() { + input.click(); + // input.click(); + } + }; + + let border_class = move || match logo_b64.with(|u| u.is_none()) { + true => "relative w-20 h-20 rounded-full border-2 border-white/20".to_string(), + _ => "relative w-20 h-20 rounded-full border-2 border-primary-600".to_string(), + }; + + view! { +
+ +
+
+ +
+ + "Add custom logo" + +
+ + +
+ +
+
} + > + +
+ +
+ + +
+ +
+
+ + } +} + +macro_rules! input_component { + ($name:ident, $input_element:ident, $input_type:ident, $attrs:expr) => { + #[component] + fn $name Option + 'static + Copy>( + #[prop(into)] heading: String, + #[prop(into)] placeholder: String, + #[prop(optional)] initial_value: Option, + #[prop(optional, into)] input_type: Option, + updater: U, + validator: V, + ) -> impl IntoView { + let ctx: CreateTokenCtx = expect_context(); + let error = create_rw_signal(initial_value.is_none()); + let show_error = create_rw_signal(false); + if error.get_untracked() { + ctx.invalid_cnt.update(|c| *c += 1); + } + let input_ref = create_node_ref::(); + let on_input = move || { + let Some(input) = input_ref() else { + return; + }; + let value = input.value(); + match validator(value) { + Some(v) => { + if error.get_untracked() { + ctx.invalid_cnt.update(|c| *c -= 1); + } + error.set(false); + updater(v); + }, + None => { + show_error.set(true); + if error.get_untracked() { + return; + } + error.set(true); + ctx.invalid_cnt.update(|c| *c += 1); + } + } + }; + create_effect(move |prev| { + ctx.on_form_reset.track(); + // Do not trigger on render + if prev.is_none() { + return; + } + let cur_show_err = show_error.get_untracked(); + on_input(); + // this is necessary + // if the user had not previously input anything, + // we don't want to show an error + if !cur_show_err { + show_error.set(false); + } + }); + + let input_class =move || match show_error() && error() { + false => format!("w-full p-3 md:p-4 md:py-5 text-white outline-none bg-white/10 border-2 border-solid border-white/20 text-xs rounded-xl placeholder-neutral-600"), + _ => format!("w-full p-3 md:p-4 md:py-5 text-white outline-none bg-white/10 border-2 border-solid border-red-500 text-xs rounded-xl placeholder-neutral-600") + }; + view! { +
+ {heading.clone()} + <$input_element + _ref=input_ref + value={initial_value.unwrap_or_default()} + on:input=move |_| on_input() + placeholder=placeholder + class=move || input_class() + type=input_type.unwrap_or_else(|| "text".into() ) + /> + + + "Invalid " + + +
+ } + } + } +} + +fn non_empty_string_validator(s: String) -> Option { + (!s.is_empty()).then_some(s) +} + +fn non_empty_string_validator_for_u64(s: String) -> Option { + if s.is_empty() { + return None; + } + s.parse().ok() +} + +input_component!(InputBox, input, Input, {}); +input_component!(InputArea, textarea, Textarea, rows = 4); +input_component!(InputField, textarea, Textarea, rows = 1); + +#[derive(Clone, Copy, Default)] +pub struct CreateTokenCtx { + form_state: RwSignal, + invalid_cnt: RwSignal, + on_form_reset: Trigger, +} + +impl CreateTokenCtx { + pub fn reset() { + let ctx: Self = expect_context(); + + ctx.form_state.set(SnsFormState::default()); + ctx.invalid_cnt.set(0); + } +} + +fn parse_token_e8s(s: &str) -> Result { + let e8s: u64 = s + .replace('_', "") + .parse::() + .map_err(|err| err.to_string())?; + + Ok(Tokens { e8s: Some(e8s) }) +} + +#[component] +pub fn CreateToken() -> impl IntoView { + let auth_cans = auth_canisters_store(); + + let ctx: CreateTokenCtx = expect_context(); + + let set_token_name = move |name: String| { + ctx.form_state.update(|f| f.name = Some(name)); + }; + let set_token_symbol = move |symbol: String| { + ctx.form_state.update(|f| f.symbol = Some(symbol)); + }; + let set_token_desc = move |desc: String| { + ctx.form_state.update(|f| f.description = Some(desc)); + }; + let set_total_distribution = move |total: u64| { + ctx.form_state.update(|f| { + (*f).try_update_total_distribution_tokens( + parse_tokens(&format!("{} tokens", total)).unwrap(), + ); + }); + }; + + let cans_wire_res = authenticated_canisters(); + + let create_action = create_action(move |&()| { + let cans_wire_res = cans_wire_res.clone(); + async move { + let cans_wire = cans_wire_res + .wait_untracked() + .await + .map_err(|e| e.to_string())?; + let cans = cans_wire + .clone() + .canisters() + .map_err(|_| "Unable to authenticate".to_string())?; + + let sns_form = ctx.form_state.get_untracked(); + let sns_config = sns_form.try_into_config(&cans)?; + + let create_sns = sns_config.try_convert_to_executed_sns_init()?; + let server_available = is_server_available().await.map_err(|e| e.to_string())?; + log::debug!( + "Server details: {}, {}", + server_available.0, + server_available.1 + ); + if !server_available.0 { + return Err("Server is not available".to_string()); + } + + TokenCreationStarted.send_event(create_sns.clone(), auth_cans); + + let deployed_cans_response = deploy_cdao_canisters(cans_wire, create_sns.clone()) + .await + .map_err(|e| e.to_string()); + + match deployed_cans_response.clone() { + Ok(_) => TokenCreationCompleted.send_event(create_sns, auth_cans), + Err(e) => TokenCreationFailed.send_event(e, create_sns, auth_cans), + } + + deployed_cans_response + } + }); + let creating = create_action.pending(); + + let create_disabled = create_memo(move |_| { + creating() + || auth_cans.with(|c| c.is_none()) + || ctx.form_state.with(|f| f.logo_b64.is_none()) + || ctx.form_state.with(|f: &SnsFormState| f.name.is_none()) + || ctx + .form_state + .with(|f: &SnsFormState| f.description.is_none()) + || ctx.form_state.with(|f| f.symbol.is_none()) + || ctx.invalid_cnt.get() != 0 + }); + + view! { +
+ + <div class="flex justify-between w-full"> + <div></div> + <span class="font-bold justify-self-center">Create Meme Token</span> + <a href="/token/create/faq"> + <img src="/img/info.svg"/> + </a> + </div> + +
+
+ + +
+ + + + + + +
+ +
+ + +
+ + +
+ } +} + +#[component] +pub fn CreateTokenSettings() -> impl IntoView { + let fallback_url = "/token/create"; + let ctx: CreateTokenCtx = use_context().unwrap_or_else(|| { + let ctx = CreateTokenCtx::default(); + provide_context(ctx); + ctx + }); + let fstate = ctx.form_state; + + let validate_tokens = |value: String| parse_tokens(&value).ok(); + let validate_tokens_e8s = |value: String| parse_token_e8s(&value).ok(); + let (transaction_fee, set_transaction_fee) = slice!(fstate.transaction_fee); + let (rejection_fee, set_rejection_fee) = slice!(fstate.proposals.rejection_fee); + + let validate_duration = |value: String| parse_duration(&value).ok(); + let (initial_voting_period, set_initial_voting_period) = + slice!(fstate.proposals.initial_voting_period); + let (max_wait_deadline_extension, set_max_wait_deadline_extension) = + slice!(fstate.proposals.maximum_wait_for_quiet_deadline_extension); + let (min_creation_stake, set_min_creation_stake) = + slice!(fstate.neurons.minimum_creation_stake); + let (min_dissolve_delay, set_min_dissolve_delay) = slice!(fstate.voting.minimum_dissolve_delay); + let (age, set_age) = slice!(fstate.voting.maximum_voting_power_bonuses.age.duration); + + let validate_percentage = |value: String| parse_percentage(&value).ok(); + let (age_bonus, set_age_bonus) = slice!(fstate.voting.maximum_voting_power_bonuses.age.bonus); + let (min_participants, set_min_participants) = slice!(fstate.swap.minimum_participants); + + let optional_tokens_validator = |value: String| { + if value.is_empty() { + return Some(None); + } + Some(Some(parse_tokens(&value).ok()?)) + }; + let (min_direct_participants_icp, set_min_direct_participants_icp) = + slice!(fstate.swap.minimum_direct_participation_icp); + let (max_direct_participants_icp, set_max_direct_participants_icp) = + slice!(fstate.swap.maximum_direct_participation_icp); + let (min_participants_icp, set_min_participants_icp) = + slice!(fstate.swap.minimum_participant_icp); + let (max_participants_icp, set_max_participants_icp) = + slice!(fstate.swap.maximum_participant_icp); + + // let set_restricted_country = move |value: String| { + // ctx.form_state.update(|f| { + // f.sns_form_setting.restricted_country = Some(value); + // }); + // }; + + let form_ref = create_node_ref::(); + let reset_settings = move |_| { + let Some(form) = form_ref() else { return }; + form.reset(); + // ctx.form_state.update(|f| f.reset_advanced_settings()); + ctx.on_form_reset.notify(); + }; + + view! { +
+ + <div class="flex justify-between w-full" style="background: black"> + <BackButton fallback=fallback_url/> + <span class="font-bold justify-self-center">Settings</span> + <a href="/token/create/faq"> + <img src="/img/info.svg"/> + </a> + </div> + +
} } + +#[component] +pub fn UploadPostPage() -> impl IntoView { + if show_cdao_page() { + view! { + + } + } else { + view! { + + } + } +} diff --git a/ssr/src/page/upload/video_upload.rs b/ssr/src/page/upload/video_upload.rs index 7d92596c..5f3e8fdf 100644 --- a/ssr/src/page/upload/video_upload.rs +++ b/ssr/src/page/upload/video_upload.rs @@ -11,11 +11,12 @@ use crate::{ VideoUploadSuccessful, VideoUploadUnsuccessful, VideoUploadVideoSelected, }, route::go_to_root, + web::FileWithUrl, MockPartialEq, }, }; use futures::StreamExt; -use gloo::{file::ObjectUrl, timers::future::IntervalStream}; +use gloo::timers::future::IntervalStream; use ic_agent::Identity; use leptos::{ ev::durationchange, @@ -40,20 +41,6 @@ pub fn DropBox() -> impl IntoView { } } -#[derive(Clone)] -pub struct FileWithUrl { - file: gloo::file::File, - url: ObjectUrl, -} - -impl FileWithUrl { - #[cfg(feature = "hydrate")] - fn new(file: gloo::file::File) -> Self { - let url = ObjectUrl::from(file.clone()); - Self { file, url } - } -} - #[component] pub fn PreVideoUpload(file_blob: WriteSignal>) -> impl IntoView { let file_ref = create_node_ref::(); diff --git a/ssr/src/page/view_profile_redirect.rs b/ssr/src/page/view_profile_redirect.rs new file mode 100755 index 00000000..308ca3f6 --- /dev/null +++ b/ssr/src/page/view_profile_redirect.rs @@ -0,0 +1,11 @@ +use crate::component::{canisters_prov::AuthCansProvider, spinner::FullScreenSpinner}; +use leptos::*; +use leptos_router::Redirect; +#[component] +pub fn ProfileInfo() -> impl IntoView { + view! { + + + + } +} diff --git a/ssr/src/page/wallet/mod.rs b/ssr/src/page/wallet/mod.rs index f0346d3f..aa00c5ca 100644 --- a/ssr/src/page/wallet/mod.rs +++ b/ssr/src/page/wallet/mod.rs @@ -1,10 +1,13 @@ +pub mod tokens; pub mod transactions; mod txn; +use crate::{component::share_popup::ShareButtonWithFallbackPopup, utils::host::get_host}; +use candid::Principal; use leptos::*; +use tokens::{TokenRootList, TokenView}; use crate::{ component::{ - back_btn::BackButton, bullet_loader::BulletLoader, canisters_prov::AuthCansProvider, connect::ConnectLogin, @@ -19,17 +22,28 @@ use txn::{provider::get_history_provider, TxnView}; #[component] fn ProfileGreeter(details: ProfileDetails) -> impl IntoView { // let (is_connected, _) = account_connected_reader(); + let share_link = { + let base_url = get_host(); + let username_or_principal = details.username_or_principal(); + format!("{base_url}/profile/{}?tab=tokens", username_or_principal) + }; + let message = format!( + "Hey! Check out my YRAL profile 👇 {}. I just minted my own token—come see and create yours! 🚀 #YRAL #TokenMinter", + share_link + ); view! {
Welcome! - - {details.display_name_or_fallback()} - +
+ + // TEMP: Workaround for hydration bug until leptos 0.7 + // class=("md:w-5/12", move || !is_connected()) + {details.display_name_or_fallback()} + + + +
@@ -55,6 +69,43 @@ fn BalanceFallback() -> impl IntoView { view! {
} } +#[component] +fn TokensFetch() -> impl IntoView { + let auth_cans = authenticated_canisters(); + let tokens_fetch = auth_cans.derive( + || (), + |cans_wire, _| async move { + let cans = cans_wire?.canisters()?; + let user_principal = cans.user_principal(); + + let tokens_prov = TokenRootList(cans); + let tokens = tokens_prov.get_by_cursor(0, 5).await?; + Ok::<_, ServerFnError>((user_principal, tokens.data)) + }, + ); + + view! { + + {move || { + tokens_fetch() + .map(|tokens_res| { + let tokens = tokens_res.as_ref().map(|t| t.1.clone()).unwrap_or_default(); + let user_principal = tokens_res + .as_ref() + .map(|t| t.0) + .unwrap_or(Principal::anonymous()); + view! { + + + + } + }) + }} + + + } +} + #[component] pub fn Wallet() -> impl IntoView { let (is_connected, _) = account_connected_reader(); @@ -64,7 +115,7 @@ pub fn Wallet() -> impl IntoView { || (), |cans_wire, _| async move { let cans = cans_wire?.canisters()?; - let user = cans.authenticated_user().await?; + let user = cans.authenticated_user().await; let bal = user.get_utility_token_balance().await?; Ok::<_, ServerFnError>(bal.to_string()) @@ -83,11 +134,6 @@ pub fn Wallet() -> impl IntoView { view! {
-
-
- -
-
@@ -98,11 +144,10 @@ pub fn Wallet() -> impl IntoView { Your Coyns Balance {move || { - let balance = try_or_redirect_opt!(balance_fetch()?); - Some(view! { -
{balance}
- }) + let balance = try_or_redirect_opt!(balance_fetch() ?); + Some(view! {
{balance}
}) }} +
@@ -115,6 +160,17 @@ pub fn Wallet() -> impl IntoView {
+
+
+ My Tokens + + See All + +
+
+ +
+
Recent Transactions @@ -124,11 +180,21 @@ pub fn Wallet() -> impl IntoView {
- {move || history_fetch().map(|history| view! { - - - - })} + {move || { + history_fetch() + .map(|history| { + view! { + + + + } + }) + }} +
diff --git a/ssr/src/page/wallet/tokens.rs b/ssr/src/page/wallet/tokens.rs new file mode 100644 index 00000000..75d23714 --- /dev/null +++ b/ssr/src/page/wallet/tokens.rs @@ -0,0 +1,179 @@ +use candid::Principal; +use ic_agent::AgentError; + +use crate::page::wallet::ShareButtonWithFallbackPopup; +use crate::utils::host::get_host; +use crate::{ + canister::individual_user_template::Result14, + component::{ + back_btn::BackButton, + bullet_loader::BulletLoader, + canisters_prov::AuthCansProvider, + claim_tokens::ClaimTokensOrRedirectError, + infinite_scroller::{CursoredDataProvider, InfiniteScroller, KeyedData, PageEntry}, + title::Title, + }, + state::canisters::{unauth_canisters, Canisters}, + utils::{ + profile::propic_from_principal, + token::{token_metadata_by_root, TokenBalance, TokenMetadata}, + }, +}; +use leptos::*; + +#[derive(Clone)] +pub struct TokenRootList(pub Canisters); + +impl KeyedData for Principal { + type Key = Principal; + + fn key(&self) -> Self::Key { + *self + } +} + +impl CursoredDataProvider for TokenRootList { + type Data = Principal; + type Error = AgentError; + + async fn get_by_cursor( + &self, + start: usize, + end: usize, + ) -> Result, Self::Error> { + let user = self.0.authenticated_user().await; + let tokens = user + .get_token_roots_of_this_user_with_pagination_cursor(start as u64, end as u64) + .await?; + let tokens = match tokens { + Result14::Ok(v) => v, + Result14::Err(_) => vec![], + }; + let list_end = tokens.len() < (end - start); + Ok(PageEntry { + data: tokens, + end: list_end, + }) + } +} + +async fn token_metadata_or_fallback( + cans: Canisters, + user_principal: Principal, + token_root: Principal, +) -> TokenMetadata { + let metadata = token_metadata_by_root(&cans, user_principal, token_root) + .await + .ok() + .flatten(); + metadata.unwrap_or_else(|| TokenMetadata { + logo_b64: propic_from_principal(token_root), + name: "".to_string(), + description: "Unknown".to_string(), + symbol: "??".to_string(), + balance: TokenBalance::new_cdao(0u32.into()), + fees: TokenBalance::new_cdao(0u32.into()), + }) +} + +#[component] +fn FallbackToken() -> impl IntoView { + view! { +
+ } +} + +#[component] +pub fn TokenView( + user_principal: Principal, + token_root: Principal, + #[prop(optional)] _ref: NodeRef, +) -> impl IntoView { + let cans = unauth_canisters(); + + let info = create_resource( + || (), + move |_| token_metadata_or_fallback(cans.clone(), user_principal, token_root), + ); + + let share_link = { + let base_url = get_host(); + format!("{base_url}/profile/{}?tab=tokens", user_principal.to_text()) + }; + let share_link_s = store_value(share_link); + let share_message = format!( + "Hey! Check out my YRAL profile 👇 {}. I just minted my own token—come see and create yours! 🚀 #YRAL #TokenMinter", + share_link_s(), + ); + let share_message_s = store_value(share_message); + + view! { + + + {move || { + info.map(|info| { + view! { + +
+ + {info.name.clone()} +
+
+ + {format!("{} {}", info.balance.humanize(), info.symbol)} + + +
+
+ } + }) + }} + +
+ } +} + +#[component] +fn TokenList(canisters: Canisters) -> impl IntoView { + // let user_canister = canisters.user_canister(); + let user_principal = canisters.user_principal(); + let provider: TokenRootList = TokenRootList(canisters); + + view! { +
+ } + } + /> + +
+ } +} + +#[component] +pub fn Tokens() -> impl IntoView { + view! { +
+ + <div class="flex flex-row justify-between"> + <BackButton fallback="/wallet".to_string()/> + <span class="text-xl text-white font-bold">Tokens</span> + <div></div> + </div> + + + + +
+ } +} diff --git a/ssr/src/page/wallet/txn.rs b/ssr/src/page/wallet/txn.rs index 2d0b7822..d6a2622c 100644 --- a/ssr/src/page/wallet/txn.rs +++ b/ssr/src/page/wallet/txn.rs @@ -153,7 +153,7 @@ pub mod provider { mod canister { use super::{Canisters, CursoredDataProvider, TxnInfo, TxnTag}; use crate::canister::individual_user_template::{ - HotOrNotOutcomePayoutEvent, MintEvent, Result7, TokenEvent, + HotOrNotOutcomePayoutEvent, MintEvent, Result15, TokenEvent, }; use crate::component::infinite_scroller::PageEntry; use ic_agent::AgentError; @@ -205,7 +205,7 @@ pub mod provider { start: usize, end: usize, ) -> Result, AgentError> { - let user = self.0.authenticated_user().await?; + let user = self.0.authenticated_user().await; let history = user .get_user_utility_token_transaction_history_with_pagination( start as u64, @@ -213,8 +213,8 @@ pub mod provider { ) .await?; let history = match history { - Result7::Ok(v) => v, - Result7::Err(_) => vec![], + Result15::Ok(v) => v, + Result15::Err(_) => vec![], }; let list_end = history.len() < (end - start); Ok(PageEntry { @@ -234,7 +234,7 @@ pub mod provider { ChaCha8Rng, }; - use crate::{component::infinite_scroller::PageEntry, utils::current_epoch}; + use crate::{component::infinite_scroller::PageEntry, utils::time::current_epoch}; use super::*; diff --git a/ssr/src/state/admin_canisters.rs b/ssr/src/state/admin_canisters.rs index d5cac21b..70709708 100644 --- a/ssr/src/state/admin_canisters.rs +++ b/ssr/src/state/admin_canisters.rs @@ -1,9 +1,11 @@ use candid::Principal; -use ic_agent::{AgentError, Identity}; +use ic_agent::Identity; use leptos::expect_context; use crate::{ - canister::{individual_user_template::IndividualUserTemplate, user_index::UserIndex}, + canister::{ + individual_user_template::IndividualUserTemplate, sns_swap::SnsSwap, user_index::UserIndex, + }, utils::ic::AgentWrapper, }; @@ -19,20 +21,30 @@ impl AdminCanisters { } } - pub async fn user_index_with( - &self, - idx_principal: Principal, - ) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(UserIndex(idx_principal, agent)) + pub fn principal(&self) -> Principal { + self.agent.principal().unwrap() + } + + pub async fn get_agent(&self) -> &ic_agent::Agent { + self.agent.get_agent().await + } + + pub async fn user_index_with(&self, idx_principal: Principal) -> UserIndex<'_> { + let agent = self.agent.get_agent().await; + UserIndex(idx_principal, agent) } pub async fn individual_user_for( &self, user_canister: Principal, - ) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(IndividualUserTemplate(user_canister, agent)) + ) -> IndividualUserTemplate<'_> { + let agent = self.agent.get_agent().await; + IndividualUserTemplate(user_canister, agent) + } + + pub async fn sns_swap(&self, swap_canister: Principal) -> SnsSwap<'_> { + let agent = self.agent.get_agent().await; + SnsSwap(swap_canister, agent) } } diff --git a/ssr/src/state/canisters.rs b/ssr/src/state/canisters.rs index 28ab4afe..2269a066 100644 --- a/ssr/src/state/canisters.rs +++ b/ssr/src/state/canisters.rs @@ -1,26 +1,32 @@ use std::sync::Arc; -use candid::Principal; +use candid::{Decode, Principal}; use ic_agent::{identity::DelegatedIdentity, AgentError, Identity}; use leptos::*; use serde::{Deserialize, Serialize}; +use sns_validation::pbs::sns_pb::SnsInitPayload; use yral_metadata_client::MetadataClient; use yral_metadata_types::UserMetadata; use crate::{ auth::DelegatedIdentityWire, canister::{ - individual_user_template::{IndividualUserTemplate, Result9, UserCanisterDetails}, + individual_user_template::{ + IndividualUserTemplate, Result23, Result7, UserCanisterDetails, + }, platform_orchestrator::PlatformOrchestrator, post_cache::PostCache, - user_index::UserIndex, + sns_governance::SnsGovernance, + sns_ledger::SnsLedger, + sns_root::SnsRoot, + user_index::{Result1, UserIndex}, PLATFORM_ORCHESTRATOR_ID, POST_CACHE_ID, }, - consts::{FALLBACK_USER_INDEX, METADATA_API_BASE}, + consts::METADATA_API_BASE, utils::{ic::AgentWrapper, profile::ProfileDetails, MockPartialEq, ParentResource}, }; -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct CanistersAuthWire { id: DelegatedIdentityWire, user_canister: Principal, @@ -106,10 +112,24 @@ impl Canisters { self.user_canister } - pub async fn authenticated_user(&self) -> Result, AgentError> { + pub async fn authenticated_user(&self) -> IndividualUserTemplate<'_> { self.individual_user(self.user_canister).await } + pub async fn deploy_cdao_sns( + &self, + init_payload: SnsInitPayload, + ) -> Result { + let agent = self.agent.get_agent().await; + let args = candid::encode_args((init_payload, 90_u64)).unwrap(); + let bytes = agent + .update(&self.user_canister, "deploy_cdao_sns") + .with_arg(args) + .call_and_wait() + .await?; + Ok(Decode!(&bytes, Result7)?) + } + pub fn profile_details(&self) -> ProfileDetails { self.profile_details .clone() @@ -124,30 +144,24 @@ impl Canisters { } impl Canisters { - pub async fn post_cache(&self) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(PostCache(POST_CACHE_ID, agent)) + pub async fn post_cache(&self) -> PostCache<'_> { + let agent = self.agent.get_agent().await; + PostCache(POST_CACHE_ID, agent) } - pub async fn individual_user( - &self, - user_canister: Principal, - ) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(IndividualUserTemplate(user_canister, agent)) + pub async fn individual_user(&self, user_canister: Principal) -> IndividualUserTemplate<'_> { + let agent = self.agent.get_agent().await; + IndividualUserTemplate(user_canister, agent) } - pub async fn user_index_with( - &self, - subnet_principal: Principal, - ) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(UserIndex(subnet_principal, agent)) + pub async fn user_index_with(&self, subnet_principal: Principal) -> UserIndex<'_> { + let agent = self.agent.get_agent().await; + UserIndex(subnet_principal, agent) } - pub async fn orchestrator(&self) -> Result, AgentError> { - let agent = self.agent.get_agent().await?; - Ok(PlatformOrchestrator(PLATFORM_ORCHESTRATOR_ID, agent)) + pub async fn orchestrator(&self) -> PlatformOrchestrator<'_> { + let agent = self.agent.get_agent().await; + PlatformOrchestrator(PLATFORM_ORCHESTRATOR_ID, agent) } pub async fn get_individual_canister_by_user_principal( @@ -161,12 +175,35 @@ impl Canisters { if let Some(meta) = meta { return Ok(Some(meta.user_canister_id)); } - // Fallback to oldest user index - let user_idx = self.user_index_with(*FALLBACK_USER_INDEX).await?; - let can = user_idx - .get_user_canister_id_from_user_principal_id(user_principal) - .await?; - Ok(can) + #[cfg(any(feature = "local-bin", feature = "local-lib"))] + { + Ok(None) + } + #[cfg(not(any(feature = "local-bin", feature = "local-lib")))] + { + use crate::consts::FALLBACK_USER_INDEX; + // Fallback to oldest user index + let user_idx = self.user_index_with(*FALLBACK_USER_INDEX).await; + let can = user_idx + .get_user_canister_id_from_user_principal_id(user_principal) + .await?; + Ok(can) + } + } + + pub async fn sns_governance(&self, canister_id: Principal) -> SnsGovernance<'_> { + let agent = self.agent.get_agent().await; + SnsGovernance(canister_id, agent) + } + + pub async fn sns_ledger(&self, canister_id: Principal) -> SnsLedger<'_> { + let agent = self.agent.get_agent().await; + SnsLedger(canister_id, agent) + } + + pub async fn sns_root(&self, canister_id: Principal) -> SnsRoot<'_> { + let agent = self.agent.get_agent().await; + SnsRoot(canister_id, agent) } async fn subnet_indexes(&self) -> Result, AgentError> { @@ -181,7 +218,7 @@ impl Canisters { // TODO: this is temporary let blacklisted = HashSet::from([Principal::from_text("rimrc-piaaa-aaaao-aaljq-cai").unwrap()]); - let orchestrator = self.orchestrator().await?; + let orchestrator = self.orchestrator().await; Ok(orchestrator .get_all_available_subnet_orchestrators() .await? @@ -209,10 +246,14 @@ async fn create_individual_canister( let discrim = u128::from_be_bytes(by); let subnet_idx = subnet_idxs[(discrim % subnet_idxs.len() as u128) as usize]; - let idx = canisters.user_index_with(subnet_idx).await?; - let user_canister = idx - .get_requester_principals_canister_id_create_if_not_exists_and_optionally_allow_referrer() - .await?; + let idx = canisters.user_index_with(subnet_idx).await; + let user_canister = match idx + .get_requester_principals_canister_id_create_if_not_exists() + .await? + { + Result1::Ok(val) => Ok(val), + Result1::Err(e) => Err(ServerFnError::new(e)), + }?; canisters .metadata_client @@ -244,7 +285,7 @@ pub async fn do_canister_auth( create_individual_canister(&canisters).await? }; - let user = canisters.authenticated_user().await?; + let user = canisters.authenticated_user().await; if let Some(referrer_principal_id) = referrer { let referrer_canister = canisters @@ -264,8 +305,8 @@ pub async fn do_canister_auth( .await .map_err(|e| e.to_string()) { - Ok(Result9::Ok(_)) => (), - Err(e) | Ok(Result9::Err(e)) => log::warn!("Failed to update last access time: {}", e), + Ok(Result23::Ok(_)) => (), + Err(e) | Ok(Result23::Err(e)) => log::warn!("Failed to update last access time: {}", e), } let profile_details = user.get_profile_details().await?.into(); diff --git a/ssr/src/state/mod.rs b/ssr/src/state/mod.rs index 4976123b..17c17de3 100644 --- a/ssr/src/state/mod.rs +++ b/ssr/src/state/mod.rs @@ -33,5 +33,7 @@ pub mod server { pub google_oauth_clients: crate::auth::core_clients::CoreClients, #[cfg(feature = "ga4")] pub grpc_offchain_channel: tonic::transport::Channel, + #[cfg(feature = "firestore")] + pub firestore_db: firestore::FirestoreDb, } } diff --git a/ssr/src/utils/event_streaming/events.rs b/ssr/src/utils/event_streaming/events.rs index bc3cff89..0618564e 100644 --- a/ssr/src/utils/event_streaming/events.rs +++ b/ssr/src/utils/event_streaming/events.rs @@ -1,9 +1,11 @@ +use candid::Principal; use ic_agent::Identity; use leptos::html::Input; use leptos::{create_effect, MaybeSignal, ReadSignal, RwSignal, SignalGetUntracked}; use leptos::{create_signal, ev, expect_context, html::Video, NodeRef, SignalGet, SignalSet}; use leptos_use::use_event_listener; use serde_json::json; +use sns_validation::pbs::sns_pb::SnsInitPayload; use wasm_bindgen::JsCast; use super::EventHistory; @@ -35,6 +37,11 @@ pub enum AnalyticsEvent { LogoutConfirmation(LogoutConfirmation), ErrorEvent(ErrorEvent), ProfileViewVideo(ProfileViewVideo), + TokenCreationStarted(TokenCreationStarted), + TokenCreationCompleted(TokenCreationCompleted), + TokenCreationFailed(TokenCreationFailed), + TokensClaimedFromNeuron(TokensClaimedFromNeuron), + TokensTransferred(TokensTransferred), } #[derive(Default)] @@ -726,3 +733,155 @@ impl ProfileViewVideo { } } } + +#[derive(Default)] +pub struct TokenCreationStarted; + +impl TokenCreationStarted { + pub fn send_event( + &self, + sns_init_payload: SnsInitPayload, + cans_store: RwSignal>>, + ) { + #[cfg(all(feature = "hydrate", feature = "ga4"))] + { + let user = user_details_can_store_or_ret!(cans_store); + let details = user.details; + + let user_id = details.principal; + let canister_id = user.canister_id; + + // token_creation_started - analytics + send_event( + "token_creation_started", + &json!({ + "user_id": user_id, + "canister_id": canister_id, + "token_name": sns_init_payload.token_name, + "token_symbol": sns_init_payload.token_symbol, + "name": sns_init_payload.name + }), + ); + } + } +} + +#[derive(Default)] +pub struct TokenCreationCompleted; + +impl TokenCreationCompleted { + pub fn send_event( + &self, + sns_init_payload: SnsInitPayload, + cans_store: RwSignal>>, + ) { + #[cfg(all(feature = "hydrate", feature = "ga4"))] + { + let user = user_details_can_store_or_ret!(cans_store); + let details = user.details; + + let user_id = details.principal; + let canister_id = user.canister_id; + + // token_creation_completed - analytics + send_event( + "token_creation_completed", + &json!({ + "user_id": user_id, + "canister_id": canister_id, + "token_name": sns_init_payload.token_name, + "token_symbol": sns_init_payload.token_symbol, + "name": sns_init_payload.name, + "description": sns_init_payload.description, + "logo": sns_init_payload.logo + }), + ); + } + } +} + +#[derive(Default)] +pub struct TokenCreationFailed; + +impl TokenCreationFailed { + pub fn send_event( + &self, + error_str: String, + sns_init_payload: SnsInitPayload, + cans_store: RwSignal>>, + ) { + #[cfg(all(feature = "hydrate", feature = "ga4"))] + { + let user = user_details_can_store_or_ret!(cans_store); + let details = user.details; + + let user_id = details.principal; + let canister_id = user.canister_id; + + // token_creation_failed - analytics + send_event( + "token_creation_failed", + &json!({ + "user_id": user_id, + "canister_id": canister_id, + "token_name": sns_init_payload.token_name, + "token_symbol": sns_init_payload.token_symbol, + "name": sns_init_payload.name, + "description": sns_init_payload.description, + "error": error_str + }), + ); + } + } +} + +#[derive(Default)] +pub struct TokensClaimedFromNeuron; + +impl TokensClaimedFromNeuron { + pub fn send_event(&self, amount: u64, cans_store: Canisters) { + #[cfg(all(feature = "hydrate", feature = "ga4"))] + { + let details = cans_store.profile_details(); + + let user_id = details.principal; + let canister_id = cans_store.user_canister(); + + // tokens_claimed_from_neuron - analytics + send_event( + "tokens_claimed_from_neuron", + &json!({ + "user_id": user_id, + "canister_id": canister_id, + "amount": amount + }), + ); + } + } +} + +#[derive(Default)] +pub struct TokensTransferred; + +impl TokensTransferred { + pub fn send_event(&self, amount: String, to: Principal, cans_store: Canisters) { + #[cfg(all(feature = "hydrate", feature = "ga4"))] + { + let details = cans_store.profile_details(); + + let user_id = details.principal; + let canister_id = cans_store.user_canister(); + + // tokens_transferred - analytics + send_event( + "tokens_transferred", + &json!({ + "user_id": user_id, + "canister_id": canister_id, + "amount": amount, + "to": to + }), + ); + } + } +} diff --git a/ssr/src/utils/event_streaming/mod.rs b/ssr/src/utils/event_streaming/mod.rs index cf6c185b..c567390d 100644 --- a/ssr/src/utils/event_streaming/mod.rs +++ b/ssr/src/utils/event_streaming/mod.rs @@ -27,7 +27,7 @@ pub struct EventHistory { #[cfg(feature = "ga4")] pub fn send_event(event_name: &str, params: &serde_json::Value) { - use super::posts::get_host; + use super::host::get_host; let event_history: EventHistory = expect_context(); @@ -60,7 +60,7 @@ pub fn send_user_id(user_id: String) { #[cfg(feature = "ga4")] pub fn send_event_warehouse(event_name: &str, params: &serde_json::Value) { - use super::posts::get_host; + use super::host::get_host; let event_name = event_name.to_string(); diff --git a/ssr/src/utils/host.rs b/ssr/src/utils/host.rs new file mode 100644 index 00000000..e4ed590d --- /dev/null +++ b/ssr/src/utils/host.rs @@ -0,0 +1,25 @@ +pub fn get_host() -> String { + #[cfg(feature = "hydrate")] + { + use leptos::window; + window().location().host().unwrap().to_string() + } + + #[cfg(not(feature = "hydrate"))] + { + use axum::http::request::Parts; + use leptos::expect_context; + + let parts: Parts = expect_context(); + let headers = parts.headers; + headers.get("Host").unwrap().to_str().unwrap().to_string() + } +} + +pub fn show_cdao_page() -> bool { + let host = get_host(); + host == "icpump.fun" + || host == "localhost:3000" + // || host == "hot-or-not-web-leptos-ssr-staging.fly.dev" + || host.contains("go-bazzinga-hot-or-not-web-leptos-ssr.fly.dev") // Use this when testing icpump changes +} diff --git a/ssr/src/utils/ic.rs b/ssr/src/utils/ic.rs index fa42a156..1a5220a8 100644 --- a/ssr/src/utils/ic.rs +++ b/ssr/src/utils/ic.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use ic_agent::{agent::AgentBuilder, Agent, AgentError, Identity}; +use candid::Principal; +use ic_agent::{agent::AgentBuilder, Agent, Identity}; use crate::consts::AGENT_URL; @@ -14,16 +15,23 @@ impl AgentWrapper { Self(builder.build().unwrap()) } - pub async fn get_agent(&self) -> Result<&Agent, AgentError> { + pub async fn get_agent(&self) -> &Agent { let agent = &self.0; #[cfg(any(feature = "local-bin", feature = "local-lib"))] { - agent.fetch_root_key().await?; + agent + .fetch_root_key() + .await + .expect("AGENT: fetch_root_key failed"); } - Ok(agent) + agent } pub fn set_arc_id(&mut self, id: Arc) { self.0.set_arc_identity(id); } + + pub fn principal(&self) -> Result { + self.0.get_principal() + } } diff --git a/ssr/src/utils/mod.rs b/ssr/src/utils/mod.rs index 8e40bb94..753262e5 100644 --- a/ssr/src/utils/mod.rs +++ b/ssr/src/utils/mod.rs @@ -1,10 +1,10 @@ use futures::{Future, StreamExt}; use leptos::{create_memo, Resource, Serializable, Signal, SignalStream, SignalWith}; use serde::{Deserialize, Serialize}; -use web_time::{Duration, SystemTime}; pub mod ab_testing; pub mod event_streaming; +pub mod host; pub mod ic; pub mod icon; pub mod ml_feed; @@ -13,17 +13,12 @@ pub mod posts; pub mod profile; pub mod report; pub mod route; -pub mod timestamp; +pub mod time; +pub mod token; pub mod types; pub mod user; pub mod web; -pub fn current_epoch() -> Duration { - web_time::SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() -} - /// Wrapper for PartialEq that always returns false /// this is currently only used for resources /// this does not provide a sane implementation of PartialEq diff --git a/ssr/src/utils/notifications/setup-firebase-messaging.js b/ssr/src/utils/notifications/setup-firebase-messaging.js index 71bda8f9..e484f8e3 100644 --- a/ssr/src/utils/notifications/setup-firebase-messaging.js +++ b/ssr/src/utils/notifications/setup-firebase-messaging.js @@ -1,25 +1,29 @@ -import 'https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js'; -import 'https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js'; +import "https://www.gstatic.com/firebasejs/9.2.0/firebase-app-compat.js"; +import "https://www.gstatic.com/firebasejs/9.2.0/firebase-messaging-compat.js"; firebase.initializeApp({ - apiKey: "AIzaSyCwo0EWTJz_w-J1lUf9w9NcEBdLNmGUaIo", - authDomain: "hot-or-not-feed-intelligence.firebaseapp.com", - projectId: "hot-or-not-feed-intelligence", - storageBucket: "hot-or-not-feed-intelligence.appspot.com", - messagingSenderId: "82502260393", - appId: "1:82502260393:web:390e9d4e588cba65237bb8" + apiKey: "AIzaSyCwo0EWTJz_w-J1lUf9w9NcEBdLNmGUaIo", + authDomain: "hot-or-not-feed-intelligence.firebaseapp.com", + projectId: "hot-or-not-feed-intelligence", + storageBucket: "hot-or-not-feed-intelligence.appspot.com", + messagingSenderId: "82502260393", + appId: "1:82502260393:web:390e9d4e588cba65237bb8", }); -let vapidKey = "BOmsEya6dANYUoElzlUWv3Jekmw08_nqDEUFu06aTak-HQGd-G_Lsk8y4Bs9B4kcEjBM8FXF0IQ_oOpJDmU3zMs"; +let vapidKey = + "BOmsEya6dANYUoElzlUWv3Jekmw08_nqDEUFu06aTak-HQGd-G_Lsk8y4Bs9B4kcEjBM8FXF0IQ_oOpJDmU3zMs"; export function get_token() { - return new Promise((resolve, reject) => { - const messaging = firebase.messaging(); - messaging.getToken({ vapidKey: vapidKey }).then((currentToken) => { - resolve(currentToken); - }).catch((err) => { - console.log('An error occurred while retrieving token. ', err); - return reject('An error occurred while retrieving token.'); - }); - }); -} \ No newline at end of file + return new Promise((resolve, reject) => { + const messaging = firebase.messaging(); + messaging + .getToken({ vapidKey: vapidKey }) + .then((currentToken) => { + resolve(currentToken); + }) + .catch((err) => { + console.log("An error occurred while retrieving token. ", err); + return reject("An error occurred while retrieving token."); + }); + }); +} diff --git a/ssr/src/utils/posts.rs b/ssr/src/utils/posts.rs index 1430b84a..a0b4d1ee 100644 --- a/ssr/src/utils/posts.rs +++ b/ssr/src/utils/posts.rs @@ -54,7 +54,7 @@ impl FetchCursor { } } -#[derive(Clone, PartialEq, Debug, Hash, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Clone, PartialEq, Ord, PartialOrd, Debug, Hash, Eq, Serialize, Deserialize)] pub struct PostDetails { pub canister_id: Principal, // canister id of the publishing canister. pub post_id: u64, @@ -116,7 +116,7 @@ pub async fn get_post_uid( user_canister: Principal, post_id: u64, ) -> Result, PostViewError> { - let post_creator_can = canisters.individual_user(user_canister).await?; + let post_creator_can = canisters.individual_user(user_canister).await; let post_details = match post_creator_can .get_individual_post_details_by_id(post_id) .await @@ -155,38 +155,19 @@ pub async fn get_post_uid( ))) } -pub fn get_feed_component_identifier() -> impl Fn() -> Option<&'static str> { - move || { - let loc = get_host(); - - if loc == "yral.com" - || loc == "localhost:3000" - || loc == "hotornot.wtf" - || loc.contains("go-bazzinga-hot-or-not-web-leptos-ssr.fly.dev") - // || loc == "hot-or-not-web-leptos-ssr-staging.fly.dev" - { - Some("PostViewWithUpdatesMLFeed") - } else { - Some("PostViewWithUpdates") - } - } -} - -pub fn get_host() -> String { - #[cfg(feature = "hydrate")] - { - use leptos::window; - window().location().host().unwrap().to_string() - } - - #[cfg(not(feature = "hydrate"))] - { - use axum::http::request::Parts; - use http::header::HeaderMap; - use leptos::expect_context; - - let parts: Parts = expect_context(); - let headers = parts.headers; - headers.get("Host").unwrap().to_str().unwrap().to_string() - } -} +// pub fn get_feed_component_identifier() -> impl Fn() -> Option<&'static str> { +// move || { +// let loc = get_host(); + +// if loc == "yral.com" +// || loc == "localhost:3000" +// || loc == "hotornot.wtf" +// || loc.contains("go-bazzinga-hot-or-not-web-leptos-ssr.fly.dev") +// // || loc == "hot-or-not-web-leptos-ssr-staging.fly.dev" +// { +// Some("PostViewWithUpdatesMLFeed") +// } else { +// Some("PostViewWithUpdates") +// } +// } +// } diff --git a/ssr/src/utils/profile.rs b/ssr/src/utils/profile.rs index 5b209256..d3740c7b 100644 --- a/ssr/src/utils/profile.rs +++ b/ssr/src/utils/profile.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::{ canister::individual_user_template::{ - BetDirection, BetOutcomeForBetMaker, PlacedBetDetail, Result5, + BetDirection, BetOutcomeForBetMaker, PlacedBetDetail, Result11, UserProfileDetailsForFrontend, }, component::infinite_scroller::{CursoredDataProvider, KeyedData, PageEntry}, @@ -15,9 +15,9 @@ use crate::{ state::canisters::Canisters, }; -use super::{current_epoch, posts::PostDetails}; +use super::{posts::PostDetails, time::current_epoch}; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ProfileDetails { pub username: Option, pub lifetime_earnings: u64, @@ -216,14 +216,14 @@ impl CursoredDataProvider for PostsProvider { start: usize, end: usize, ) -> Result, AgentError> { - let user = self.canisters.individual_user(self.user).await?; + let user = self.canisters.individual_user(self.user).await; let limit = end - start; let posts = user .get_posts_of_this_user_profile_with_pagination_cursor(start as u64, limit as u64) .await?; let posts = match posts { - Result5::Ok(v) => v, - Result5::Err(_) => { + Result11::Ok(v) => v, + Result11::Err(_) => { log::warn!("failed to get posts"); return Ok(PageEntry { data: vec![], @@ -268,7 +268,7 @@ impl CursoredDataProvider for BetsProvider { start: usize, end: usize, ) -> Result, AgentError> { - let user = self.canisters.individual_user(self.user).await?; + let user = self.canisters.individual_user(self.user).await; assert_eq!(end - start, 10); let bets = user .get_hot_or_not_bets_placed_by_this_profile_with_pagination(start as u64) diff --git a/ssr/src/utils/timestamp.rs b/ssr/src/utils/time.rs similarity index 65% rename from ssr/src/utils/timestamp.rs rename to ssr/src/utils/time.rs index 0743eeaa..046bcbdc 100644 --- a/ssr/src/utils/timestamp.rs +++ b/ssr/src/utils/time.rs @@ -1,5 +1,11 @@ use uts2ts::uts2ts; -use web_time::Duration; +use web_time::{Duration, SystemTime}; + +pub fn current_epoch() -> Duration { + web_time::SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() +} /// Get day & month -> DD MMM format /// where DD -> 2 digits @@ -32,3 +38,16 @@ pub fn to_hh_mm_ss(duration: Duration) -> String { format!("{hh:02}:{mm:02}:{ss:02}") } + +pub async fn sleep(duration: Duration) { + #[cfg(feature = "ssr")] + { + use tokio::time; + time::sleep(duration).await; + } + #[cfg(feature = "hydrate")] + { + use gloo::timers::future::sleep; + sleep(duration).await; + } +} diff --git a/ssr/src/utils/token/icpump.rs b/ssr/src/utils/token/icpump.rs new file mode 100644 index 00000000..0fdaffbb --- /dev/null +++ b/ssr/src/utils/token/icpump.rs @@ -0,0 +1,99 @@ +use serde::{Deserialize, Serialize}; + +use futures::stream::BoxStream; +use futures::StreamExt; + +use leptos::*; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenListItemFS { + pub user_id: String, + pub name: String, + pub token_name: String, + pub token_symbol: String, + pub logo: String, + pub description: String, + pub created_at: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TokenListItem { + pub user_id: String, + pub name: String, + pub token_name: String, + pub token_symbol: String, + pub logo: String, + pub description: String, + pub created_at: String, + pub formatted_created_at: String, +} + +#[server] +pub async fn get_paginated_token_list(page: u32) -> Result, ServerFnError> { + #[cfg(feature = "firestore")] + { + use firestore::*; + use speedate::DateTime; + + use crate::consts::ICPUMP_LISTING_PAGE_SIZE; + + let firestore_db: firestore::FirestoreDb = expect_context(); + + const TEST_COLLECTION_NAME: &str = "tokens-list"; //"test-tokens-3" + + let object_stream: BoxStream = firestore_db + .fluent() + .select() + .from(TEST_COLLECTION_NAME) + .order_by([( + path!(TokenListItem::created_at), + FirestoreQueryDirection::Descending, + )]) + .offset((page - 1) * ICPUMP_LISTING_PAGE_SIZE as u32) + .limit(ICPUMP_LISTING_PAGE_SIZE as u32) + .obj() + .stream_query() + .await + .expect("failed to stream"); + + let as_vec: Vec = object_stream.collect().await; + + let res_vec: Vec = as_vec + .iter() + .map(|item| { + let created_at_str = item.created_at.clone(); + let created_at = DateTime::parse_str(&created_at_str).unwrap().timestamp(); + let now = DateTime::now(0).unwrap().timestamp(); + let elapsed = now - created_at; + + let elapsed_str = if elapsed < 60 { + format!("{}s ago", elapsed) + } else if elapsed < 3600 { + format!("{}m ago", elapsed / 60) + } else if elapsed < 86400 { + format!("{}h ago", elapsed / 3600) + } else { + format!("{}d ago", elapsed / 86400) + }; + + TokenListItem { + user_id: item.user_id.clone(), + name: item.name.clone(), + token_name: item.token_name.clone(), + token_symbol: item.token_symbol.clone(), + logo: item.logo.clone(), + description: item.description.clone(), + created_at: item.created_at.clone(), + formatted_created_at: elapsed_str, + } + }) + .collect(); + + Ok(res_vec) + } + + #[cfg(not(feature = "firestore"))] + { + Ok(vec![]) + } +} diff --git a/ssr/src/utils/token/mod.rs b/ssr/src/utils/token/mod.rs new file mode 100644 index 00000000..ac6cbde2 --- /dev/null +++ b/ssr/src/utils/token/mod.rs @@ -0,0 +1,287 @@ +pub mod icpump; +#[cfg(feature = "ssr")] +mod server_impl; + +use std::{ + cmp::Ordering, + ops::{Add, AddAssign, Sub, SubAssign}, + str::FromStr, +}; + +use candid::{Encode, Nat, Principal}; +use ic_agent::AgentError; +use leptos::ServerFnError; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +use crate::{ + canister::{ + sns_governance::{DissolveState, GetMetadataArg, ListNeurons, Neuron, SnsGovernance}, + sns_ledger::Account as LedgerAccount, + sns_root::ListSnsCanistersArg, + }, + state::canisters::{Canisters, CanistersAuthWire}, +}; +use leptos::{server, server_fn::codec::Cbor}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TokenBalance { + pub e8s: Nat, + decimals: u8, +} + +impl TokenBalance { + pub fn new(e8s: Nat, decimals: u8) -> Self { + Self { e8s, decimals } + } + + /// Token Balance but with 8 decimals (default for Cdao) + pub fn new_cdao(e8s: Nat) -> Self { + Self::new(e8s, 8u8) + } + + /// Parse a numeric value + /// multiplied by 8 decimals (1e8) + pub fn parse_cdao(token_str: &str) -> Result { + let tokens = (Decimal::from_str(token_str)? * Decimal::new(1e8 as i64, 0)).floor(); + let e8s = Nat::from_str(&tokens.to_string()).unwrap(); + Ok(Self::new_cdao(e8s)) + } + + // Human friendly token amount + pub fn humanize(&self) -> String { + (self.e8s.clone() / 10u64.pow(self.decimals as u32)) + .to_string() + .replace("_", ",") + } + + // Humanize the amount, but as a float + pub fn humanize_float(&self) -> String { + let tokens = Decimal::from_str(&self.e8s.0.to_str_radix(10)).unwrap() + / Decimal::new(10i64.pow(self.decimals as u32), 0); + tokens.to_string() + } + + // Returns number of tokens(not e8s) + pub fn to_tokens(&self) -> String { + let tokens = self.e8s.clone() / Nat::from(10u64.pow(self.decimals as u32)); + tokens.0.to_str_radix(10) + } +} + +impl From for Nat { + fn from(value: TokenBalance) -> Nat { + value.e8s + } +} + +impl Add for TokenBalance { + type Output = Self; + + fn add(self, other: Nat) -> Self { + Self { + e8s: self.e8s + other, + decimals: self.decimals, + } + } +} + +impl AddAssign for TokenBalance { + fn add_assign(&mut self, rhs: Nat) { + self.e8s += rhs; + } +} + +impl PartialEq for TokenBalance { + fn eq(&self, other: &Nat) -> bool { + self.e8s.eq(other) + } +} + +impl PartialOrd for TokenBalance { + fn partial_cmp(&self, other: &Nat) -> Option { + self.e8s.partial_cmp(other) + } +} + +impl Sub for TokenBalance { + type Output = Self; + + fn sub(self, rhs: Nat) -> Self { + Self { + e8s: self.e8s - rhs, + decimals: self.decimals, + } + } +} + +impl SubAssign for TokenBalance { + fn sub_assign(&mut self, rhs: Nat) { + self.e8s -= rhs; + } +} + +impl Sub for TokenBalance { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self { + e8s: self.e8s - rhs.e8s, + decimals: self.decimals, + } + } +} + +impl SubAssign for TokenBalance { + fn sub_assign(&mut self, rhs: TokenBalance) { + self.e8s -= rhs.e8s; + } +} + +impl PartialEq for TokenBalance { + fn eq(&self, other: &Self) -> bool { + self.e8s.eq(&other.e8s) + } +} + +impl PartialOrd for TokenBalance { + fn partial_cmp(&self, other: &Self) -> Option { + self.e8s.partial_cmp(&other.e8s) + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct TokenMetadata { + pub logo_b64: String, + pub name: String, + pub description: String, + pub symbol: String, + pub balance: TokenBalance, + pub fees: TokenBalance, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct TokenCans { + pub governance: Principal, + pub ledger: Principal, + pub root: Principal, +} + +pub async fn token_metadata_by_root( + cans: &Canisters, + user_principal: Principal, + token_root: Principal, +) -> Result, ServerFnError> { + // let user_principal = cans + let root = cans.sns_root(token_root).await; + let sns_cans = root.list_sns_canisters(ListSnsCanistersArg {}).await?; + let Some(governance) = sns_cans.governance else { + return Ok(None); + }; + let Some(ledger) = sns_cans.ledger else { + return Ok(None); + }; + let metadata = get_token_metadata(cans, user_principal, governance, ledger).await?; + + Ok(Some(metadata)) +} + +pub async fn get_token_metadata( + cans: &Canisters, + user_principal: Principal, + governance: Principal, + ledger: Principal, +) -> Result { + let governance = cans.sns_governance(governance).await; + let metadata = governance.get_metadata(GetMetadataArg {}).await?; + + let ledger = cans.sns_ledger(ledger).await; + let symbol = ledger.icrc_1_symbol().await?; + + let acc = LedgerAccount { + owner: user_principal, + subaccount: None, + }; + let balance_e8s = ledger.icrc_1_balance_of(acc).await?; + let fees = ledger.icrc_1_fee().await?; + + Ok(TokenMetadata { + logo_b64: metadata.logo.unwrap_or_default(), + name: metadata.name.unwrap_or_default(), + description: metadata.description.unwrap_or_default(), + symbol, + fees: TokenBalance::new_cdao(fees), + balance: TokenBalance::new_cdao(balance_e8s), + }) +} + +#[server(input = Cbor)] +pub async fn claim_tokens_from_first_neuron( + cans_wire: CanistersAuthWire, + governance_principal: Principal, + ledger_principal: Principal, + raw_neuron: Vec, +) -> Result<(), ServerFnError> { + server_impl::claim_tokens_from_first_neuron( + cans_wire, + governance_principal, + ledger_principal, + raw_neuron, + ) + .await +} + +async fn get_neurons( + governance: &SnsGovernance<'_>, + user_principal: Principal, +) -> Result, ServerFnError> { + let neurons = governance + .list_neurons(ListNeurons { + of_principal: Some(user_principal), + limit: 10, + start_page_at: None, + }) + .await?; + + Ok(neurons.neurons) +} + +pub async fn claim_tokens_from_first_neuron_if_required( + cans_wire: CanistersAuthWire, + token_root: Principal, +) -> Result<(), ServerFnError> { + let cans = cans_wire.clone().canisters()?; + let root_canister = cans.sns_root(token_root).await; + let token_cans = root_canister + .list_sns_canisters(ListSnsCanistersArg {}) + .await?; + let Some(governance) = token_cans.governance else { + log::warn!("No governance canister found for token. Ignoring..."); + return Ok(()); + }; + let Some(ledger) = token_cans.ledger else { + log::warn!("No ledger canister found for token. Ignoring..."); + return Ok(()); + }; + + let governance_can = cans.sns_governance(governance).await; + + let neurons = get_neurons(&governance_can, cans.user_principal()).await?; + if neurons.len() < 2 || neurons[1].cached_neuron_stake_e8s == 0 { + return Ok(()); + } + let ix = if matches!( + neurons[1].dissolve_state.as_ref(), + Some(DissolveState::DissolveDelaySeconds(0)) + ) { + 1 + } else { + 0 + }; + if neurons[ix].cached_neuron_stake_e8s == 0 { + return Ok(()); + } + + let raw_neurons = Encode!(&neurons[ix]).unwrap(); + claim_tokens_from_first_neuron(cans_wire, governance, ledger, raw_neurons).await +} diff --git a/ssr/src/utils/token/server_impl.rs b/ssr/src/utils/token/server_impl.rs new file mode 100644 index 00000000..e3b6ced0 --- /dev/null +++ b/ssr/src/utils/token/server_impl.rs @@ -0,0 +1,94 @@ +use crate::{ + canister::{ + sns_governance::{Account, Amount, Command, Command1, Disburse, ManageNeuron, Neuron}, + sns_ledger::{Account as LedgerAccount, TransferArg, TransferResult}, + }, + state::canisters::CanistersAuthWire, + utils::event_streaming::events::TokensClaimedFromNeuron, +}; +use candid::{Decode, Nat, Principal}; +use leptos::ServerFnError; + +// TODO: XX: this should happen at token creation to prevent races +pub async fn claim_tokens_from_first_neuron( + cans_wire: CanistersAuthWire, + governance_principal: Principal, + ledger_principal: Principal, + raw_neuron: Vec, +) -> Result<(), ServerFnError> { + let cans = cans_wire.canisters()?; + let user_principal = cans.user_principal(); + log::trace!("!!!!! Claiming tokens from first neuron"); + log::trace!("!!!!! user_principal: {:?}", user_principal); + log::trace!("!!!!! root: {:?}", governance_principal); + + let governance = cans.sns_governance(governance_principal).await; + let neuron = Decode!(&raw_neuron, Neuron)?; + + let neuron_id = neuron + .id + .map(|id| id.id) + .ok_or_else(|| ServerFnError::new("invalid neuron"))?; + let amount = neuron.cached_neuron_stake_e8s; + if amount == 0 { + return Ok(()); + } + let manage_neuron_arg = ManageNeuron { + subaccount: neuron_id, + command: Some(Command::Disburse(Disburse { + to_account: Some(Account { + owner: Some(user_principal), + subaccount: None, + }), + amount: Some(Amount { e8s: amount }), + })), + }; + let manage_neuron = governance.manage_neuron(manage_neuron_arg).await?; + match manage_neuron.command { + Some(Command1::Disburse(_)) => { + TokensClaimedFromNeuron.send_event(amount, cans.clone()); + } + Some(Command1::Error(e)) => { + return Err(ServerFnError::new(format!( + "failed to claim tokens: {}", + e.error_message + ))) + } + _ => return Err(ServerFnError::new("Failed to claim tokens")), + } + + // Transfer to canister + let user_canister = cans.user_canister(); + let ledger = cans.sns_ledger(ledger_principal).await; + // User has 50% of the overall amount + // 20% of this 50% is 10% of the overall amount + // 10% of the overall amount is reserveed for the canister + let distribution_amt = Nat::from(amount) * 20u32 / 100u32; + let transfer_resp = ledger + .icrc_1_transfer(TransferArg { + to: LedgerAccount { + owner: user_canister, + subaccount: None, + }, + fee: None, + memo: None, + from_subaccount: None, + amount: distribution_amt, + created_at_time: None, + }) + .await; + + match transfer_resp { + Ok(TransferResult::Err(e)) => { + log::error!("Token is in invalid state, user_canister: {user_canister}, governance: {governance_principal}, irrecoverable {e:?}"); + return Err(ServerFnError::new("Failed to transfer to user canister")); + } + Err(e) => { + log::error!("Token is in invalid state, user_canister: {user_canister}, governance: {governance_principal}, irrecoverable {e}"); + return Err(ServerFnError::new("Failed to transfer to user canister")); + } + _ => (), + } + + Ok(()) +} diff --git a/ssr/src/utils/web.rs b/ssr/src/utils/web.rs index 5dfcd400..3f622a24 100644 --- a/ssr/src/utils/web.rs +++ b/ssr/src/utils/web.rs @@ -1,5 +1,20 @@ +use gloo::file::ObjectUrl; use leptos_use::use_window; +#[derive(Clone)] +pub struct FileWithUrl { + pub file: gloo::file::File, + pub url: ObjectUrl, +} + +impl FileWithUrl { + #[cfg(feature = "hydrate")] + pub fn new(file: gloo::file::File) -> Self { + let url = ObjectUrl::from(file.clone()); + Self { file, url } + } +} + /// Share a URL with the Web Share API /// returns None if the API is not available pub fn share_url(url: &str) -> Option<()> { @@ -32,3 +47,22 @@ pub fn copy_to_clipboard(text: &str) -> Option<()> { _ = navigator.clipboard().write_text(text); Some(()) } + +/// Paste text from clipboard +/// passes None if the API is not available +/// or if the clipboard is unavailable +pub async fn paste_from_clipboard() -> Option { + #[cfg(feature = "hydrate")] + { + use wasm_bindgen_futures::JsFuture; + + let navigator = use_window().navigator()?; + let text_prom = navigator.clipboard().read_text(); + let text_val = JsFuture::from(text_prom).await.ok()?; + text_val.as_string() + } + #[cfg(not(feature = "hydrate"))] + { + None + } +} diff --git a/ssr/style/input.css b/ssr/style/input.css index 17f52330..421b3425 100644 --- a/ssr/style/input.css +++ b/ssr/style/input.css @@ -16,4 +16,20 @@ --color-primary-900: 115 2 62; --color-primary-950: 80 1 43; } +} + +@media (min-height:820px) and (max-height:899px){ + .hot-left-arrow{ + margin-top: 206px !important; + } +} +@media (min-height:768px) and (max-height:819px){ + .hot-left-arrow{ + margin-top: 166px !important; + } +} +@media (max-height:767px){ + .hot-left-arrow{ + margin-top: 75px !important; + } } \ No newline at end of file