diff --git a/Cargo.lock b/Cargo.lock index acf9e108..ec5e599a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,7 +554,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "itoa 1.0.15", + "itoa", "matchit", "memchr", "mime", @@ -850,9 +850,9 @@ dependencies = [ [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -861,9 +861,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1846,15 +1846,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.29.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", + "itoa", "matches", - "phf 0.8.0", + "phf 0.10.1", "proc-macro2", "quote", "smallvec", @@ -1878,7 +1878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", - "itoa 1.0.15", + "itoa", "ryu", "serde", ] @@ -3155,16 +3155,14 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.26.0" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", + "match_token", ] [[package]] @@ -3175,7 +3173,7 @@ checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", - "itoa 1.0.15", + "itoa", ] [[package]] @@ -3227,7 +3225,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.15", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -3632,12 +3630,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.15" @@ -3773,14 +3765,13 @@ dependencies = [ [[package]] name = "kuchikiki" -version = "0.8.2" +version = "0.8.8-speedreader" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 1.9.3", - "matches", + "indexmap 2.10.0", "selectors", ] @@ -3972,18 +3963,29 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", + "phf 0.11.3", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", ] +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "matchers" version = "0.1.0" @@ -4085,9 +4087,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", "dpi", @@ -4101,7 +4103,7 @@ dependencies = [ "png", "serde", "thiserror 2.0.12", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4754,9 +4756,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", "phf_shared 0.8.0", - "proc-macro-hack", ] [[package]] @@ -4765,7 +4765,9 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ + "phf_macros 0.10.0", "phf_shared 0.10.0", + "proc-macro-hack", ] [[package]] @@ -4790,12 +4792,12 @@ dependencies = [ [[package]] name = "phf_codegen" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -4830,12 +4832,12 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", + "phf_generator 0.10.0", + "phf_shared 0.10.0", "proc-macro-hack", "proc-macro2", "quote", @@ -5960,6 +5962,7 @@ dependencies = [ "tauri-plugin-safe-area-insets", "tauri-plugin-sage", "tauri-plugin-sharesheet", + "tauri-plugin-store", "tauri-plugin-window-state", "tauri-specta", "tokio", @@ -6054,22 +6057,20 @@ dependencies = [ [[package]] name = "selectors" -version = "0.22.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", "log", - "matches", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -6129,7 +6130,7 @@ version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ - "itoa 1.0.15", + "itoa", "memchr", "ryu", "serde", @@ -6141,7 +6142,7 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ - "itoa 1.0.15", + "itoa", "serde", ] @@ -6172,7 +6173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.15", + "itoa", "ryu", "serde", ] @@ -6231,9 +6232,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" dependencies = [ "nodrop", "stable_deref_trait", @@ -6605,7 +6606,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "itoa 1.0.15", + "itoa", "log", "md-5", "memchr", @@ -6644,7 +6645,7 @@ dependencies = [ "hkdf", "hmac", "home", - "itoa 1.0.15", + "itoa", "log", "md-5", "memchr", @@ -6822,9 +6823,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" +checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a" dependencies = [ "bitflags 2.9.0", "core-foundation", @@ -6890,17 +6891,16 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tauri" -version = "2.5.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc" +checksum = "352a4bc7bf6c25f5624227e3641adf475a6535707451b09bb83271df8b7a6ac7" dependencies = [ "anyhow", "bytes", "dirs 6.0.0", "dunce", "embed_plist", - "futures-util", - "getrandom 0.2.16", + "getrandom 0.3.2", "glob", "gtk", "heck 0.5.0", @@ -6942,9 +6942,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc" +checksum = "182d688496c06bf08ea896459bf483eb29cdff35c1c4c115fb14053514303064" dependencies = [ "anyhow", "cargo_toml", @@ -6964,9 +6964,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47" +checksum = "b54a99a6cd8e01abcfa61508177e6096a4fe2681efecee9214e962f2f073ae4a" dependencies = [ "base64 0.22.1", "brotli", @@ -6991,9 +6991,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.2.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112" +checksum = "7945b14dc45e23532f2ded6e120170bbdd4af5ceaa45784a6b33d250fbce3f9e" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -7005,9 +7005,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601" +checksum = "5bd5c1e56990c70a906ef67a9851bbdba9136d26075ee9a2b19c8b46986b3e02" dependencies = [ "anyhow", "glob", @@ -7182,6 +7182,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "tauri-plugin-store" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916c609664a56c82aeaefffca9851fd072d4d41f73d63f22ee3ee451508194f" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-window-state" version = "2.2.2" @@ -7199,9 +7215,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39" +checksum = "2b1cc885be806ea15ff7b0eb47098a7b16323d9228876afda329e34e2d6c4676" dependencies = [ "cookie", "dpi", @@ -7221,9 +7237,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.6.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2" +checksum = "fe653a2fbbef19fe898efc774bc52c8742576342a33d3d028c189b57eb1d2439" dependencies = [ "gtk", "http", @@ -7276,9 +7292,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.4.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4" +checksum = "9330c15cabfe1d9f213478c9e8ec2b0c76dab26bb6f314b8ad1c8a568c1d186e" dependencies = [ "anyhow", "brotli", @@ -7380,12 +7396,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - [[package]] name = "thiserror" version = "1.0.69" @@ -7463,7 +7473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", - "itoa 1.0.15", + "itoa", "num-conv", "powerfmt", "serde", @@ -7780,9 +7790,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" +checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" dependencies = [ "crossbeam-channel", "dirs 6.0.0", @@ -8373,9 +8383,9 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -8398,9 +8408,9 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ "thiserror 2.0.12", "windows 0.61.1", @@ -8693,6 +8703,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.2", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -8732,13 +8751,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-version" version = "0.1.4" @@ -8766,6 +8801,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -8784,6 +8825,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -8802,12 +8849,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -8826,6 +8885,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -8844,6 +8909,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -8862,6 +8933,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -8880,6 +8957,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -8950,9 +9033,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.51.2" +version = "0.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2" +checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9" dependencies = [ "base64 0.22.1", "block2 0.6.1", diff --git a/crates/sage-api/endpoints.json b/crates/sage-api/endpoints.json index f57ea282..a65174da 100644 --- a/crates/sage-api/endpoints.json +++ b/crates/sage-api/endpoints.json @@ -24,6 +24,7 @@ "get_all_cats": true, "get_token": true, "get_dids": true, + "get_profile": true, "get_minter_did_ids": true, "get_options": true, "get_option": true, diff --git a/crates/sage-api/src/requests/data.rs b/crates/sage-api/src/requests/data.rs index 9f651145..46620277 100644 --- a/crates/sage-api/src/requests/data.rs +++ b/crates/sage-api/src/requests/data.rs @@ -210,6 +210,18 @@ pub struct GetTokenResponse { pub token: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetProfile { + pub launcher_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "tauri", derive(specta::Type))] +pub struct GetProfileResponse { + pub did: Option, +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "tauri", derive(specta::Type))] pub struct GetDids {} diff --git a/crates/sage-database/src/tables/assets/did.rs b/crates/sage-database/src/tables/assets/did.rs index 925200a4..6100acb8 100644 --- a/crates/sage-database/src/tables/assets/did.rs +++ b/crates/sage-database/src/tables/assets/did.rs @@ -18,6 +18,67 @@ pub struct DidRow { } impl Database { + pub async fn owned_did(&self, hash: Bytes32) -> Result> { + let hash = hash.as_ref(); + + query!( + " + SELECT + asset_hash, asset_name, asset_ticker, asset_precision, asset_icon_url, + asset_description, asset_is_visible, asset_is_sensitive_content, + asset_hidden_puzzle_hash, owned_coins.created_height, spent_height, + parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, + metadata, recovery_list_hash, num_verifications_required, + offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, + clawback_expiration_seconds AS 'clawback_timestamp?' + FROM owned_coins + INNER JOIN dids ON dids.asset_id = owned_coins.asset_id + WHERE owned_coins.asset_hash = ? + ", + hash + ) + .fetch_optional(&self.pool) + .await? + .map(|row| { + Ok(DidRow { + asset: Asset { + hash: row.asset_hash.convert()?, + name: row.asset_name, + ticker: row.asset_ticker, + precision: row.asset_precision.convert()?, + icon_url: row.asset_icon_url, + description: row.asset_description, + is_visible: row.asset_is_visible, + is_sensitive_content: row.asset_is_sensitive_content, + hidden_puzzle_hash: row.asset_hidden_puzzle_hash.convert()?, + kind: AssetKind::Did, + }, + did_info: DidCoinInfo { + metadata: row.metadata.into(), + recovery_list_hash: row.recovery_list_hash.convert()?, + num_verifications_required: row.num_verifications_required.convert()?, + }, + coin_row: CoinRow { + coin: Coin::new( + row.parent_coin_hash.convert()?, + row.puzzle_hash.convert()?, + row.amount.convert()?, + ), + p2_puzzle_hash: row.p2_puzzle_hash.convert()?, + kind: CoinKind::Did, + mempool_item_hash: None, + offer_hash: row.offer_hash.convert()?, + clawback_timestamp: row.clawback_timestamp.map(|t| t as u64), + created_height: row.created_height.convert()?, + spent_height: row.spent_height.convert()?, + created_timestamp: row.created_timestamp.convert()?, + spent_timestamp: row.spent_timestamp.convert()?, + }, + }) + }) + .transpose() + } + pub async fn owned_dids(&self) -> Result> { query!( " @@ -27,8 +88,8 @@ impl Database { asset_hidden_puzzle_hash, owned_coins.created_height, spent_height, parent_coin_hash, puzzle_hash, amount, p2_puzzle_hash, metadata, recovery_list_hash, num_verifications_required, - offer_hash, created_timestamp, spent_timestamp, - clawback_expiration_seconds AS clawback_timestamp + offer_hash AS 'offer_hash?', created_timestamp, spent_timestamp, + clawback_expiration_seconds AS 'clawback_timestamp?' FROM owned_coins INNER JOIN dids ON dids.asset_id = owned_coins.asset_id ORDER BY asset_name ASC @@ -66,7 +127,7 @@ impl Database { kind: CoinKind::Did, mempool_item_hash: None, offer_hash: row.offer_hash.convert()?, - clawback_timestamp: row.clawback_timestamp.convert()?, + clawback_timestamp: row.clawback_timestamp.map(|t| t as u64), created_height: row.created_height.convert()?, spent_height: row.spent_height.convert()?, created_timestamp: row.created_timestamp.convert()?, diff --git a/crates/sage/src/endpoints/data.rs b/crates/sage/src/endpoints/data.rs index a49528b6..93a490bf 100644 --- a/crates/sage/src/endpoints/data.rs +++ b/crates/sage/src/endpoints/data.rs @@ -20,9 +20,9 @@ use sage_api::{ GetNftCollections, GetNftCollectionsResponse, GetNftData, GetNftDataResponse, GetNftIcon, GetNftIconResponse, GetNftResponse, GetNftThumbnail, GetNftThumbnailResponse, GetNfts, GetNftsResponse, GetOption, GetOptionResponse, GetOptions, GetOptionsResponse, - GetPendingTransactions, GetPendingTransactionsResponse, GetSpendableCoinCount, - GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, GetToken, - GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions, + GetPendingTransactions, GetPendingTransactionsResponse, GetProfile, GetProfileResponse, + GetSpendableCoinCount, GetSpendableCoinCountResponse, GetSyncStatus, GetSyncStatusResponse, + GetToken, GetTokenResponse, GetTransaction, GetTransactionResponse, GetTransactions, GetTransactionsResponse, GetVersion, GetVersionResponse, NftCollectionRecord, NftData, NftRecord, NftSortMode as ApiNftSortMode, OptionRecord, OptionSortMode as ApiOptionSortMode, PendingTransactionRecord, PerformDatabaseMaintenance, PerformDatabaseMaintenanceResponse, @@ -359,6 +359,28 @@ impl Sage { Ok(GetTokenResponse { token }) } + pub async fn get_profile(&self, req: GetProfile) -> Result { + let wallet = self.wallet()?; + + let launcher_id = parse_did_id(req.launcher_id.clone())?; + let Some(row) = wallet.db.owned_did(launcher_id).await? else { + return Ok(GetProfileResponse { did: None }); + }; + + let did = DidRecord { + launcher_id: Address::new(row.asset.hash, "did:chia:".to_string()).encode()?, + name: row.asset.name, + visible: row.asset.is_visible, + coin_id: hex::encode(row.coin_row.coin.coin_id()), + address: Address::new(row.coin_row.p2_puzzle_hash, self.network().prefix()).encode()?, + amount: Amount::u64(row.coin_row.coin.amount), + recovery_hash: row.did_info.recovery_list_hash.map(hex::encode), + created_height: row.coin_row.created_height, + }; + + Ok(GetProfileResponse { did: Some(did) }) + } + pub async fn get_dids(&self, _req: GetDids) -> Result { let wallet = self.wallet()?; diff --git a/package.json b/package.json index 9f3162bc..7ce1d98b 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@tauri-apps/plugin-fs": "~2", "@tauri-apps/plugin-opener": "^2.2.6", "@tauri-apps/plugin-os": "^2.2.1", + "@tauri-apps/plugin-store": "^2.3.0", "@use-gesture/react": "^10.3.1", "@walletconnect/sign-client": "^2.17.2", "@walletconnect/types": "^2.17.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70368c41..a56b0a28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@tauri-apps/plugin-os': specifier: ^2.2.1 version: 2.2.1 + '@tauri-apps/plugin-store': + specifier: ^2.3.0 + version: 2.3.0 '@use-gesture/react': specifier: ^10.3.1 version: 10.3.1(react@18.3.1) @@ -1696,6 +1699,9 @@ packages: '@tauri-apps/api@2.3.0': resolution: {integrity: sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==} + '@tauri-apps/api@2.7.0': + resolution: {integrity: sha512-v7fVE8jqBl8xJFOcBafDzXFc8FnicoH3j8o8DNNs0tHuEBmXUDqrCOAzMRX0UkfpwqZLqvrvK0GNQ45DfnoVDg==} + '@tauri-apps/cli-darwin-arm64@2.3.1': resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==} engines: {node: '>= 10'} @@ -1782,6 +1788,9 @@ packages: '@tauri-apps/plugin-os@2.2.1': resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==} + '@tauri-apps/plugin-store@2.3.0': + resolution: {integrity: sha512-mre8er0nXPhyEWQzWCpUd+UnEoBQYcoA5JYlwpwOV9wcxKqlXTGfminpKsE37ic8NUb2BIZqf0QQ9/U3ib2+/A==} + '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -5670,6 +5679,8 @@ snapshots: '@tauri-apps/api@2.3.0': {} + '@tauri-apps/api@2.7.0': {} + '@tauri-apps/cli-darwin-arm64@2.3.1': optional: true @@ -5741,6 +5752,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.3.0 + '@tauri-apps/plugin-store@2.3.0': + dependencies: + '@tauri-apps/api': 2.7.0 + '@types/estree@1.0.5': {} '@types/istanbul-lib-coverage@2.0.6': {} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 098b9738..e0d1b19e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,6 +41,7 @@ reqwest = { workspace = true } # https://aws.github.io/aws-lc-rs/platform_support.html#tested-platforms aws-lc-rs = { version = "1", features = ["bindgen"] } tauri-plugin-sharesheet = "0.0.1" +tauri-plugin-store = "2.3.0" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-window-state = { workspace = true } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 67c5e4fe..427545a4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -14,6 +14,9 @@ "clipboard-manager:default", "clipboard-manager:allow-write-text", "clipboard-manager:allow-read-text", - "opener:default" + "opener:default", + "store:default", + "store:allow-load", + "store:allow-save" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 510465d1..96a01251 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -71,6 +71,7 @@ pub fn run() { commands::get_all_cats, commands::get_token, commands::get_dids, + commands::get_profile, commands::get_minter_did_ids, commands::get_options, commands::get_option, @@ -146,7 +147,8 @@ pub fn run() { let mut tauri_builder = tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_os::init()); + .plugin(tauri_plugin_os::init()) + .plugin(tauri_plugin_store::Builder::new().build()); #[cfg(not(mobile))] { diff --git a/src/App.tsx b/src/App.tsx index 4da07468..9282fc22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import { WalletConnectProvider } from './contexts/WalletConnectContext'; import { WalletProvider } from './contexts/WalletContext'; import useInitialization from './hooks/useInitialization'; import { useTransactionFailures } from './hooks/useTransactionFailures'; +import { initializeMintGardenService } from './lib/mintGardenConfig'; import { loadCatalog } from './i18n'; import Addresses from './pages/Addresses'; import CollectionMetaData from './pages/CollectionMetaData'; @@ -54,6 +55,7 @@ import { TokenList } from './pages/TokenList'; import Transaction from './pages/Transaction'; import { Transactions } from './pages/Transactions'; import Wallet from './pages/Wallet'; +import Profile from './pages/Profile'; const router = createHashRouter( createRoutesFromElements( @@ -83,6 +85,7 @@ const router = createHashRouter( }> } /> } /> + } /> }> } /> @@ -170,6 +173,11 @@ function AppInner() { // Enable global transaction failure handling useTransactionFailures(); + // Initialize MintGarden service with rate limiting configuration + useEffect(() => { + initializeMintGardenService(); + }, []); + useEffect(() => { const initLocale = async () => { await loadCatalog(locale); diff --git a/src/bindings.ts b/src/bindings.ts index 68b56f7b..55fdd923 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -149,6 +149,9 @@ async getToken(req: GetToken) : Promise { async getDids(req: GetDids) : Promise { return await TAURI_INVOKE("get_dids", { req }); }, +async getProfile(req: GetProfile) : Promise { + return await TAURI_INVOKE("get_profile", { req }); +}, async getMinterDidIds(req: GetMinterDidIds) : Promise { return await TAURI_INVOKE("get_minter_did_ids", { req }); }, @@ -468,6 +471,8 @@ export type GetPeers = Record export type GetPeersResponse = { peers: PeerRecord[] } export type GetPendingTransactions = Record export type GetPendingTransactionsResponse = { transactions: PendingTransactionRecord[] } +export type GetProfile = { launcher_id: string } +export type GetProfileResponse = { did: DidRecord | null } export type GetSecretKey = { fingerprint: number } export type GetSecretKeyResponse = { secrets: SecretKeyInfo | null } export type GetSpendableCoinCount = { asset_id: string | null } diff --git a/src/components/AssetIcon.tsx b/src/components/AssetIcon.tsx index 14e88d06..874423fd 100644 --- a/src/components/AssetIcon.tsx +++ b/src/components/AssetIcon.tsx @@ -7,14 +7,16 @@ export interface AssetIconProps { kind: AssetKind; revocation_address: string | null; }; - size?: 'sm' | 'md' | 'lg'; + size?: 'sm' | 'md' | 'lg' | 'xl' | 'xxl'; className?: string; } const sizeClasses = { - sm: 'max-w-6 max-h-6', - md: 'max-w-8 max-h-8', - lg: 'max-w-10 max-h-10', + sm: 'w-6 h-6', + md: 'w-8 h-8', + lg: 'w-10 h-10', + xl: 'w-16 h-16', + xxl: 'w-24 h-24', }; export function AssetIcon({ diff --git a/src/components/DidInfo.tsx b/src/components/DidInfo.tsx new file mode 100644 index 00000000..e0649640 --- /dev/null +++ b/src/components/DidInfo.tsx @@ -0,0 +1,23 @@ +import ProfileCard from '@/components/ProfileCard'; +import { t } from '@lingui/core/macro'; +import { AddressItem } from './AddressItem'; +export interface DidInfoProps { + did?: string | null; + title: string; + className?: string; +} + +export function DidInfo({ did, title, className = '' }: DidInfoProps) { + if (!did) { + return ( + + ); + } + + return ( +
+ + +
+ ); +} diff --git a/src/components/NftCardList.tsx b/src/components/NftCardList.tsx index 9ddc8883..306084b5 100644 --- a/src/components/NftCardList.tsx +++ b/src/components/NftCardList.tsx @@ -2,8 +2,9 @@ import { NftCard } from '@/components/NftCard'; import { NftGroupCard } from '@/components/NftGroupCard'; import { NO_COLLECTION_ID } from '@/hooks/useNftData'; import { CardSize, NftGroupMode } from '@/hooks/useNftParams'; +import { mintGardenService } from '@/lib/mintGardenService'; import { t } from '@lingui/core/macro'; -import { ReactNode, useCallback } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import { commands, DidRecord, @@ -71,103 +72,133 @@ export function NftCardList({ [multiSelect, setSelected], ); - const renderContent = () => { - if ( - !collectionId && - !ownerDid && - !minterDid && - group === NftGroupMode.Collection - ) { - return ( - <> - {collections.map((col) => ( - { - commands - .updateNftCollection({ - collection_id: col.collection_id, - visible: !col.visible, - }) - .then(() => updateNfts(page)) - .catch(addError); - }} - setSplitNftOffers={setSplitNftOffers} - /> - ))} - - ); - } + const [content, setContent] = useState(null); - if ( - !collectionId && - !ownerDid && - !minterDid && - group === NftGroupMode.OwnerDid - ) { - return ( - <> - {ownerDids.map((did) => ( - - ))} - - ); - } + useEffect(() => { + const loadContent = async () => { + if ( + !collectionId && + !ownerDid && + !minterDid && + group === NftGroupMode.Collection + ) { + setContent( + <> + {collections.map((col) => ( + { + commands + .updateNftCollection({ + collection_id: col.collection_id, + visible: !col.visible, + }) + .then(() => updateNfts(page)) + .catch(addError); + }} + setSplitNftOffers={setSplitNftOffers} + /> + ))} + , + ); + return; + } + + if ( + !collectionId && + !ownerDid && + !minterDid && + group === NftGroupMode.OwnerDid + ) { + setContent( + <> + {ownerDids.map((did) => ( + + ))} + , + ); + return; + } - if ( - !collectionId && - !ownerDid && - !minterDid && - group === NftGroupMode.MinterDid - ) { - return ( - <> - {minterDids.map((did) => ( - - ))} - + if ( + !collectionId && + !ownerDid && + !minterDid && + group === NftGroupMode.MinterDid + ) { + await mintGardenService.loadProfiles( + minterDids.map((did) => did.launcher_id), + ); + setContent( + <> + {minterDids.map((did) => ( + + ))} + , + ); + return; + } + + setContent( + nfts.map((nft) => ( + updateNfts(page)} + selectionState={ + multiSelect + ? [ + selected.includes(nft.launcher_id), + () => handleSelection(nft.launcher_id), + ] + : null + } + /> + )), ); - } + }; - return nfts.map((nft) => ( - updateNfts(page)} - selectionState={ - multiSelect - ? [ - selected.includes(nft.launcher_id), - () => handleSelection(nft.launcher_id), - ] - : null - } - /> - )); - }; + loadContent(); + }, [ + collectionId, + ownerDid, + minterDid, + group, + collections, + ownerDids, + minterDids, + nfts, + updateNfts, + page, + multiSelect, + selected, + handleSelection, + addError, + setSplitNftOffers, + ]); return (
- {renderContent()} + {content} {children}
); diff --git a/src/components/NftGroupCard.tsx b/src/components/NftGroupCard.tsx index ea4425e7..32056d8f 100644 --- a/src/components/NftGroupCard.tsx +++ b/src/components/NftGroupCard.tsx @@ -4,9 +4,10 @@ import { NftRecord, commands, } from '@/bindings'; +import { MintGardenProfile } from '@/hooks/useDidData'; import { NftGroupMode } from '@/hooks/useNftParams'; import useOfferStateWithDefault from '@/hooks/useOfferStateWithDefault'; -//import { getMintGardenProfile } from '@/lib/marketplaces'; +import { getMintGardenProfile } from '@/lib/marketplaces'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { writeText } from '@tauri-apps/plugin-clipboard-manager'; @@ -72,21 +73,12 @@ export function NftGroupCard({ const isCollection = type === 'collection'; // Profile state for DID cards - const [didProfile, setDidProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); + const [didProfile, setDidProfile] = useState(null); // Fetch profile data for DID cards useEffect(() => { if (!isCollection && isDidRecord(item)) { - setDidProfile({ - encoded_id: item.launcher_id, - name: item.name || item.launcher_id.slice(0, 8), - avatar_uri: null, - }); - //getMintGardenProfile(item.launcher_id).then(setDidProfile); + getMintGardenProfile(item.launcher_id, true).then(setDidProfile); } }, [isCollection, item]); // Type guards to help TypeScript narrow the types diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx new file mode 100644 index 00000000..d9feb36e --- /dev/null +++ b/src/components/ProfileCard.tsx @@ -0,0 +1,533 @@ +import { AssetIcon } from '@/components/AssetIcon'; +import ConfirmationDialog from '@/components/ConfirmationDialog'; +import { DidConfirmation } from '@/components/confirmations/DidConfirmation'; +import { FeeOnlyDialog } from '@/components/FeeOnlyDialog'; +import { TransferDialog } from '@/components/TransferDialog'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useDidData } from '@/hooks/useDidData'; +import { useErrors } from '@/hooks/useErrors'; +import { toMojos } from '@/lib/utils'; +import { useWalletState } from '@/state'; +import { t } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { writeText } from '@tauri-apps/plugin-clipboard-manager'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { + ActivityIcon, + Copy, + ExternalLinkIcon, + EyeIcon, + EyeOff, + Flame, + MoreVerticalIcon, + PenIcon, + SendIcon, +} from 'lucide-react'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { commands, DidRecord, TransactionResponse } from '../bindings'; +import { CustomError } from '../contexts/ErrorContext'; + +export interface ProfileCardProps { + // For MintGarden profiles (DID string only) + did: DidRecord | string; + variant?: 'default' | 'compact' | 'card'; + className?: string; + updateDids?: () => void; +} + +export function ProfileCard({ + did, + variant = 'default', + className = '', + updateDids, +}: ProfileCardProps) { + const { addError } = useErrors(); + const walletState = useWalletState(); + const navigate = useNavigate(); + + // Use the useDidData hook to get DID information and asset object + const { + did: didRecord, + didAsset, + mintGardenProfile, + isMintGardenLoading, + isOwned, + } = useDidData({ did }); + + // State for local DID actions (only when did is owned) + const [name, setName] = useState(''); + const [response, setResponse] = useState(null); + const [renameOpen, setRenameOpen] = useState(false); + const [transferOpen, setTransferOpen] = useState(false); + const [burnOpen, setBurnOpen] = useState(false); + const [normalizeOpen, setNormalizeOpen] = useState(false); + const [isTransferring, setIsTransferring] = useState(false); + const [isBurning, setIsBurning] = useState(false); + const [isNormalizing, setIsNormalizing] = useState(false); + const [transferAddress, setTransferAddress] = useState(''); + + // Owned DID action handlers + const rename = () => { + if (!name || !isOwned) return; + + commands + .updateDid({ + did_id: didRecord.launcher_id, + name, + visible: didRecord.visible, + }) + .then(() => updateDids?.()) + .catch((err) => addError(err as CustomError)) + .finally(() => { + setRenameOpen(false); + setName(''); + }); + }; + + const toggleVisibility = () => { + if (!isOwned) return; + + commands + .updateDid({ + did_id: didRecord.launcher_id, + name: didRecord.name, + visible: !didRecord.visible, + }) + .then(() => updateDids?.()) + .catch((err) => addError(err as CustomError)); + }; + + const onTransferSubmit = (address: string, fee: string) => { + if (!isOwned) return; + + setIsTransferring(true); + setTransferAddress(address); + commands + .transferDids({ + did_ids: [didRecord.launcher_id], + address, + fee: toMojos(fee, walletState.sync.unit.precision), + }) + .then(setResponse) + .catch((err) => { + setIsTransferring(false); + addError(err as CustomError); + }) + .finally(() => setTransferOpen(false)); + }; + + const onBurnSubmit = (fee: string) => { + if (!isOwned) return; + + setIsBurning(true); + commands + .transferDids({ + did_ids: [didRecord.launcher_id], + address: walletState.sync.burn_address, + fee: toMojos(fee, walletState.sync.unit.precision), + }) + .then(setResponse) + .catch((err) => { + setIsBurning(false); + addError(err as CustomError); + }) + .finally(() => setBurnOpen(false)); + }; + + const onNormalizeSubmit = (fee: string) => { + if (!isOwned) return; + + setIsNormalizing(true); + commands + .normalizeDids({ + did_ids: [didRecord.launcher_id], + fee: toMojos(fee, walletState.sync.unit.precision), + }) + .then(setResponse) + .catch((err) => { + setIsNormalizing(false); + addError(err as CustomError); + }) + .finally(() => setNormalizeOpen(false)); + }; + + const handleMintGardenClick = () => { + if (mintGardenProfile && !mintGardenProfile.is_unknown) { + openUrl(`https://mintgarden.io/${mintGardenProfile.encoded_id}`); + } + }; + + const handleDidClick = () => { + navigate(`/dids/${didRecord.launcher_id}`); + }; + + // Loading state + if (isMintGardenLoading || !didAsset || !mintGardenProfile) { + return ( +
+ +
+ +
+
+ ); + } + + if (variant === 'compact') { + return ( +
+ + {didAsset.name} + + {!mintGardenProfile.is_unknown && ( + + )} +
+ ); + } + + if (variant === 'card') { + return ( + <> + + + + +
+ {mintGardenProfile.is_unknown + ? didAsset.name + : mintGardenProfile.name}{' '} + {isOwned && + !mintGardenProfile.is_unknown && + didRecord.name && + didRecord.name !== mintGardenProfile.name && ( + + {`(${didRecord.name})`} + + )} +
+
+ {isOwned ? ( + + + + + + + {!mintGardenProfile.is_unknown && ( + <> + { + e.stopPropagation(); + handleMintGardenClick(); + }} + > + + View on MintGarden + + + + )} + + { + e.stopPropagation(); + setTransferOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Transfer + + + {didRecord.recovery_hash === null && ( + { + e.stopPropagation(); + setNormalizeOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Normalize + + )} + + { + e.stopPropagation(); + setBurnOpen(true); + }} + disabled={didRecord.created_height === null} + > + + Burn + + + + + { + e.stopPropagation(); + writeText(didRecord.launcher_id); + toast.success(t`DID ID copied to clipboard`); + }} + > + + Copy ID + + + { + e.stopPropagation(); + setRenameOpen(true); + }} + > + + Rename + + + { + e.stopPropagation(); + toggleVisibility(); + }} + > + {didRecord.visible ? ( + + ) : ( + + )} + {didRecord.visible ? t`Hide` : t`Show`} + + + + + ) : null} +
+ +
+ {didAsset.asset_id} +
+
+
+ + {isOwned && ( + <> + !open && setRenameOpen(false)} + > + + + + Rename Profile + + + Enter the new display name for this profile. + + +
+
+ + setName(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + rename(); + } + }} + /> +
+
+ + + + +
+
+ + + This will send the profile to the provided address. + + + + + This will permanently delete the profile by sending it to the + burn address. + + + + + + This will modify the profile's recovery info to be + compatible with the Chia reference wallet. + + + + { + setResponse(null); + setIsTransferring(false); + setIsBurning(false); + setIsNormalizing(false); + }} + onConfirm={() => updateDids?.()} + additionalData={ + isTransferring && response + ? { + title: t`Transfer DID`, + content: ( + + ), + } + : isBurning && response + ? { + title: t`Burn DID`, + content: ( + + ), + } + : isNormalizing && response + ? { + title: t`Normalize DID`, + content: ( + + ), + } + : undefined + } + /> + + )} + + ); + } + + return ( +
+ +
+
{didAsset.name}
+
+ {!mintGardenProfile.is_unknown && ( + + )} +
+ ); +} + +export default ProfileCard; diff --git a/src/hooks/useDidData.ts b/src/hooks/useDidData.ts new file mode 100644 index 00000000..ead0aafa --- /dev/null +++ b/src/hooks/useDidData.ts @@ -0,0 +1,157 @@ +import { getMintGardenProfile } from '@/lib/marketplaces'; +import { getAssetDisplayName } from '@/lib/utils'; +import { useEffect, useMemo, useState } from 'react'; +import { AssetKind, commands, DidRecord, events } from '../bindings'; + +export interface UseDidDataParams { + did: DidRecord | string; +} + +export interface MintGardenProfile { + encoded_id: string; + name: string; + avatar_uri: string | null; + is_unknown: boolean; +} + +export interface DidAsset { + icon_url: string | null; + kind: AssetKind; + revocation_address: null; + name: string; + ticker: string; + precision: number; + asset_id: string; + balance: string; + balanceInUsd: string; + priceInUsd: string; +} + +export function useDidData({ did: inputDid }: UseDidDataParams) { + const [fetchedDid, setFetchedDid] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [mintGardenProfile, setMintGardenProfile] = + useState(null); + const [isMintGardenLoading, setIsMintGardenLoading] = useState(false); + + // Determine if this is an owned DID and create the final DidRecord + const isOwned = typeof inputDid !== 'string'; + const launcherId = + typeof inputDid === 'string' ? inputDid : inputDid.launcher_id; + + const didRecord: DidRecord = useMemo(() => { + if (isOwned) { + return inputDid as DidRecord; + } + + // Use fetched data if available, otherwise create fallback + if (fetchedDid) { + return fetchedDid; + } + + // Fallback for string launcher_id when no fetched data + return { + launcher_id: inputDid as string, + name: `${(inputDid as string).slice(9, 19)}...${(inputDid as string).slice(-4)}`, + visible: true, + created_height: null, + recovery_hash: null, + coin_id: '0', + address: '', + amount: 0, + }; + }, [inputDid, fetchedDid, isOwned]); + + const updateDid = useMemo( + () => () => { + if (!launcherId || isOwned) return; // Don't fetch if we already have owned DID data + + setIsLoading(true); + commands + .getProfile({ launcher_id: launcherId }) + .then((data) => setFetchedDid(data.did)) + .catch(() => { + // On error, we'll use the fallback DidRecord created in the memo above + // Don't call addError here to avoid showing errors for non-owned DIDs + }) + .finally(() => setIsLoading(false)); + }, + [launcherId, isOwned], + ); + + // Fetch MintGarden profile data + useEffect(() => { + if (!didRecord?.launcher_id) return; + + setIsMintGardenLoading(true); + getMintGardenProfile(didRecord.launcher_id) + .then((profileData) => { + setMintGardenProfile(profileData); + }) + .catch(() => { + // Create fallback profile for failed lookups + setMintGardenProfile({ + encoded_id: didRecord.launcher_id, + name: didRecord.name ?? '', + avatar_uri: null, + is_unknown: true, + }); + }) + .finally(() => { + setIsMintGardenLoading(false); + }); + }, [didRecord?.launcher_id, didRecord?.name]); + + // Create DID asset object + const didAsset: DidAsset | null = useMemo(() => { + if (!didRecord || !mintGardenProfile) return null; + + return { + icon_url: mintGardenProfile.avatar_uri, + kind: 'did' as AssetKind, + revocation_address: null, + name: !mintGardenProfile.is_unknown + ? mintGardenProfile.name + : getAssetDisplayName( + didRecord.name || mintGardenProfile.name, + null, + 'did', + ), + ticker: '', + precision: 0, + asset_id: didRecord.launcher_id, + balance: '0', + balanceInUsd: '0', + priceInUsd: '0', + }; + }, [didRecord, mintGardenProfile]); + + useEffect(() => { + updateDid(); + + const unlisten = events.syncEvent.listen((event) => { + const type = event.payload.type; + if ( + type === 'coin_state' || + type === 'puzzle_batch_synced' || + type === 'did_info' + ) { + updateDid(); + } + }); + + return () => { + unlisten.then((u) => u()); + }; + }, [updateDid]); + + return { + did: didRecord, + isLoading, + updateDid, + mintGardenProfile, + isMintGardenLoading, + didAsset, + isOwned, + }; +} diff --git a/src/lib/marketplaces.ts b/src/lib/marketplaces.ts index d99f3f53..8331db81 100644 --- a/src/lib/marketplaces.ts +++ b/src/lib/marketplaces.ts @@ -55,18 +55,5 @@ export const marketplaces: MarketplaceConfig[] = [ }, ]; -export async function getMintGardenProfile(did: string) { - try { - const response = await fetch(`https://api.mintgarden.io/profile/${did}`); - const data = await response.json(); - // always supply a name - data.name = data.name || `${did.slice(9, 19)}...${did.slice(-4)}`; - return data; - } catch { - return { - encoded_id: did, - name: `${did.slice(9, 19)}...${did.slice(-4)}`, // 9 strips off "did:chia:" - avatar_uri: null, - }; - } -} +// Re-export the rate-limited and cached version +export { getMintGardenProfile } from './mintGardenService'; diff --git a/src/lib/mintGardenConfig.ts b/src/lib/mintGardenConfig.ts new file mode 100644 index 00000000..4deaa24f --- /dev/null +++ b/src/lib/mintGardenConfig.ts @@ -0,0 +1,28 @@ +import { mintGardenService } from './mintGardenService'; + +// MintGarden API rate limiting configuration +export const MINTGARDEN_CONFIG = { + // Delay between API requests in milliseconds + // Increase this value if you're getting 429 errors + DELAY_BETWEEN_REQUESTS: 1000, // 1 second + + // How long to cache profile data in milliseconds + // 15 minutes = 15 * 60 * 1000 + CACHE_DURATION: 15 * 60 * 1000, + + // Maximum number of concurrent requests + // Lower this value if you're getting 429 errors + MAX_CONCURRENT_REQUESTS: 3, +} as const; + +// Initialize the service with the configuration +export function initializeMintGardenService(): void { + mintGardenService.updateConfig({ + delayBetweenRequests: MINTGARDEN_CONFIG.DELAY_BETWEEN_REQUESTS, + cacheDuration: MINTGARDEN_CONFIG.CACHE_DURATION, + maxConcurrentRequests: MINTGARDEN_CONFIG.MAX_CONCURRENT_REQUESTS, + }); +} + +// Export the service for direct access if needed +export { mintGardenService }; diff --git a/src/lib/mintGardenService.ts b/src/lib/mintGardenService.ts new file mode 100644 index 00000000..9ff46486 --- /dev/null +++ b/src/lib/mintGardenService.ts @@ -0,0 +1,309 @@ +import { MintGardenProfile } from '@/hooks/useDidData'; +import { Store, load } from '@tauri-apps/plugin-store'; + +interface CacheEntry { + profile: MintGardenProfile; + timestamp: number; +} + +interface MintGardenServiceConfig { + delayBetweenRequests: number; // milliseconds + cacheDuration: number; // milliseconds + maxConcurrentRequests: number; +} + +class MintGardenService { + private store: Store | null = null; + private pendingRequests = new Map>(); + private lastRequestTime = 0; + private config: MintGardenServiceConfig; + private isInitialized = false; + + constructor(config: Partial = {}) { + this.config = { + delayBetweenRequests: 750, // 3/4 second default + cacheDuration: 15 * 60 * 1000, // 15 minutes default + maxConcurrentRequests: 3, // Allow 3 concurrent requests + ...config, + }; + + try { + this.initializeStore().then(() => { + this.clearExpiredCache(); + }); + } catch (error) { + console.warn('Failed to initialize MintGardenService:', error); + } + } + + private async initializeStore(): Promise { + if (this.isInitialized) return; + + try { + this.store = await load('.mintgarden-profile-cache.dat'); + } catch (error) { + console.warn('Failed to load MintGarden cache store:', error); + } finally { + this.isInitialized = true; + } + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async ensureRateLimit(): Promise { + const now = Date.now(); + const timeSinceLastRequest = now - this.lastRequestTime; + + if (timeSinceLastRequest < this.config.delayBetweenRequests) { + const delayNeeded = + this.config.delayBetweenRequests - timeSinceLastRequest; + await this.delay(delayNeeded); + } + + this.lastRequestTime = Date.now(); + } + + private isCacheValid(entry: CacheEntry): boolean { + const now = Date.now(); + return now - entry.timestamp < this.config.cacheDuration; + } + + private async getCachedProfile( + did: string, + ): Promise { + await this.initializeStore(); + + if (!this.store) { + return null; + } + + try { + const cached = await this.store.get(`profile:${did}`); + + if (cached && this.isCacheValid(cached)) { + return cached.profile; + } + } catch (error) { + console.warn('Failed to read from cache:', error); + } + + return null; + } + + private async setCachedProfile( + did: string, + profile: MintGardenProfile, + ): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const entry: CacheEntry = { + profile, + timestamp: Date.now(), + }; + await this.store.set(`profile:${did}`, entry); + await this.store.save(); + } catch (error) { + console.warn('Failed to write to cache:', error); + } + } + + private async fetchProfileFromAPI(did: string): Promise { + await this.ensureRateLimit(); + + try { + const response = await fetch(`https://api.mintgarden.io/profile/${did}`); + const data = await response.json(); + + if (data?.detail === 'Unknown profile.') { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + } + + // always supply a name + data.name = data.name || `${did.slice(9, 19)}...${did.slice(-4)}`; + data.is_unknown = false; + return data; + } catch { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + } + } + + async loadProfiles(dids: string[]): Promise { + if (dids.length === 0) { + return; + } + + await this.clearExpiredCache(); + await this.ensureRateLimit(); + + try { + const response = await fetch('https://api.mintgarden.io/profiles_by_id', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(dids), + }); + const data = await response.json(); + const profiles = data.map((profile: MintGardenProfile) => ({ + encoded_id: profile.encoded_id, + name: profile.name, + avatar_uri: profile.avatar_uri, + is_unknown: false, + })); + + // this bit goes through and fills in the profiles for the dids that are not in the response + for (const did of dids) { + const profile = profiles.find( + (p: MintGardenProfile) => p.encoded_id === did, + ) ?? { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + + await this.setCachedProfile(did, profile); + } + } catch (error) { + console.warn('Failed to load profiles:', error); + } + } + + async getProfile(did: string, cacheOnly = false): Promise { + // Check cache first + const cached = await this.getCachedProfile(did); + if (cached) { + return cached; + } + + // the cache only flag is used to avoid making a request if the profile is not in the cache + if (cacheOnly) { + return { + encoded_id: did, + name: `${did.slice(9, 19)}...${did.slice(-4)}`, + avatar_uri: null, + is_unknown: true, + }; + } + + // Check if there's already a pending request for this DID + const pendingRequest = this.pendingRequests.get(did); + if (pendingRequest) { + return pendingRequest; + } + + // Check concurrent request limit + if (this.pendingRequests.size >= this.config.maxConcurrentRequests) { + // Wait for any request to complete + await Promise.race(this.pendingRequests.values()); + // Recursive call to try again + return this.getProfile(did); + } + + // Create new request + const requestPromise = this.fetchProfileFromAPI(did).then( + async (profile) => { + // Cache the result + await this.setCachedProfile(did, profile); + + // Remove from pending requests + this.pendingRequests.delete(did); + + return profile; + }, + ); + + // Add to pending requests + this.pendingRequests.set(did, requestPromise); + + return requestPromise; + } + + // Clear cache entries that have expired + async clearExpiredCache(): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const keys = await this.store.keys(); + const profileKeys = keys.filter((key) => key.startsWith('profile:')); + + for (const key of profileKeys) { + const entry = await this.store.get(key); + if (entry && !this.isCacheValid(entry)) { + await this.store.delete(key); + } + } + + await this.store.save(); + } catch (error) { + console.warn('Failed to clear expired cache:', error); + } + } + + // Clear all cache + async clearCache(): Promise { + await this.initializeStore(); + + if (!this.store) { + return; + } + + try { + const keys = await this.store.keys(); + const profileKeys = keys.filter((key) => key.startsWith('profile:')); + + for (const key of profileKeys) { + await this.store.delete(key); + } + + await this.store.save(); + } catch (error) { + console.warn('Failed to clear cache:', error); + } + } + // Update configuration + updateConfig(newConfig: Partial): void { + this.config = { ...this.config, ...newConfig }; + } + + // Get current configuration + getConfig(): MintGardenServiceConfig { + return { ...this.config }; + } +} + +// Create a singleton instance +export const mintGardenService = new MintGardenService(); + +// Export the function that maintains backward compatibility +export async function getMintGardenProfile( + did: string, + cacheOnly = false, +): Promise { + return mintGardenService.getProfile(did, cacheOnly); +} + +// Export the service class and configuration interface for advanced usage +export { MintGardenService }; +export type { MintGardenServiceConfig }; diff --git a/src/pages/CollectionMetaData.tsx b/src/pages/CollectionMetaData.tsx index 69fd2f50..677ffd47 100644 --- a/src/pages/CollectionMetaData.tsx +++ b/src/pages/CollectionMetaData.tsx @@ -1,6 +1,7 @@ import { commands, NetworkKind, NftCollectionRecord } from '@/bindings'; import { AddressItem } from '@/components/AddressItem'; import Container from '@/components/Container'; +import { DidInfo } from '@/components/DidInfo'; import Header from '@/components/Header'; import { LabeledItem } from '@/components/LabeledItem'; import { Button } from '@/components/ui/button'; @@ -8,7 +9,6 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { CustomError } from '@/contexts/ErrorContext'; import { useErrors } from '@/hooks/useErrors'; import spacescanLogo from '@/images/spacescan-logo-192.png'; -import { getMintGardenProfile } from '@/lib/marketplaces'; import { t } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; import { openUrl } from '@tauri-apps/plugin-opener'; @@ -37,11 +37,6 @@ export default function CollectionMetaData() { useState(null); const [loading, setLoading] = useState(true); const [network, setNetwork] = useState(null); - const [minterProfile, setMinterProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); useEffect(() => { async function fetchData() { @@ -93,15 +88,6 @@ export default function CollectionMetaData() { fetchData(); }, [collection_id, addError]); - useEffect(() => { - if (!collection?.did_id) { - setMinterProfile(null); - return; - } - - getMintGardenProfile(collection.did_id).then(setMinterProfile); - }, [collection?.did_id]); - useEffect(() => { commands .getNetwork({}) @@ -417,27 +403,7 @@ export default function CollectionMetaData() { - - {minterProfile && ( -
- openUrl(`https://mintgarden.io/${collection.did_id}`) - } - > - {minterProfile.avatar_uri && ( - {`${minterProfile.name} - )} -
{minterProfile.name}
-
- )} +
diff --git a/src/pages/DidList.tsx b/src/pages/DidList.tsx index 3aec4fb9..aae63ad7 100644 --- a/src/pages/DidList.tsx +++ b/src/pages/DidList.tsx @@ -1,56 +1,16 @@ -import ConfirmationDialog from '@/components/ConfirmationDialog'; -import { DidConfirmation } from '@/components/confirmations/DidConfirmation'; import Container from '@/components/Container'; -import { FeeOnlyDialog } from '@/components/FeeOnlyDialog'; import Header from '@/components/Header'; +import { ProfileCard } from '@/components/ProfileCard'; import { ReceiveAddress } from '@/components/ReceiveAddress'; -import { TransferDialog } from '@/components/TransferDialog'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { useDids } from '@/hooks/useDids'; -import { useErrors } from '@/hooks/useErrors'; -import { toMojos } from '@/lib/utils'; -import { useWalletState } from '@/state'; import { t } from '@lingui/core/macro'; import { Plural, Trans } from '@lingui/react/macro'; -import { writeText } from '@tauri-apps/plugin-clipboard-manager'; -import { - ActivityIcon, - Copy, - EyeIcon, - EyeOff, - Flame, - MoreVerticalIcon, - PenIcon, - SendIcon, - UserIcon, - UserPlusIcon, - UserRoundPlus, -} from 'lucide-react'; +import { UserPlusIcon, UserRoundPlus } from 'lucide-react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import { commands, DidRecord, TransactionResponse } from '../bindings'; export function DidList() { const navigate = useNavigate(); @@ -103,344 +63,15 @@ export function DidList() {
{visibleDids.map((did) => ( - + ))}
); } - -interface ProfileProps { - did: DidRecord; - updateDids: () => void; -} - -function Profile({ did, updateDids }: ProfileProps) { - const { addError } = useErrors(); - - const walletState = useWalletState(); - - const [name, setName] = useState(''); - const [response, setResponse] = useState(null); - - const [renameOpen, setRenameOpen] = useState(false); - const [transferOpen, setTransferOpen] = useState(false); - const [burnOpen, setBurnOpen] = useState(false); - const [normalizeOpen, setNormalizeOpen] = useState(false); - - // Track which action is being performed - const [isTransferring, setIsTransferring] = useState(false); - const [isBurning, setIsBurning] = useState(false); - const [isNormalizing, setIsNormalizing] = useState(false); - const [transferAddress, setTransferAddress] = useState(''); - - const rename = () => { - if (!name) return; - - commands - .updateDid({ did_id: did.launcher_id, name, visible: did.visible }) - .then(updateDids) - .catch(addError) - .finally(() => { - setRenameOpen(false); - setName(''); - }); - }; - - const toggleVisibility = () => { - commands - .updateDid({ - did_id: did.launcher_id, - name: did.name, - visible: !did.visible, - }) - .then(updateDids) - .catch(addError); - }; - - const onTransferSubmit = (address: string, fee: string) => { - setIsTransferring(true); - setTransferAddress(address); - commands - .transferDids({ - did_ids: [did.launcher_id], - address, - fee: toMojos(fee, walletState.sync.unit.precision), - }) - .then(setResponse) - .catch((err) => { - setIsTransferring(false); - addError(err); - }) - .finally(() => setTransferOpen(false)); - }; - - const onBurnSubmit = (fee: string) => { - setIsBurning(true); - commands - .transferDids({ - did_ids: [did.launcher_id], - address: walletState.sync.burn_address, - fee: toMojos(fee, walletState.sync.unit.precision), - }) - .then(setResponse) - .catch((err) => { - setIsBurning(false); - addError(err); - }) - .finally(() => setBurnOpen(false)); - }; - - const onNormalizeSubmit = (fee: string) => { - setIsNormalizing(true); - commands - .normalizeDids({ - did_ids: [did.launcher_id], - fee: toMojos(fee, walletState.sync.unit.precision), - }) - .then(setResponse) - .catch((err) => { - setIsNormalizing(false); - addError(err); - }) - .finally(() => setNormalizeOpen(false)); - }; - - return ( - <> - - - - - {did.name ?? t`Untitled Profile`} - - - - - - - - { - e.stopPropagation(); - setTransferOpen(true); - }} - disabled={did.created_height === null} - > - - - Transfer - - - - {did.recovery_hash === null && ( - { - e.stopPropagation(); - setNormalizeOpen(true); - }} - disabled={did.created_height === null} - > - - - Normalize - - - )} - - { - e.stopPropagation(); - setBurnOpen(true); - }} - disabled={did.created_height === null} - > - - - Burn - - - - - - { - e.stopPropagation(); - writeText(did.launcher_id); - toast.success(t`DID ID copied to clipboard`); - }} - > - - - Copy ID - - - - { - e.stopPropagation(); - setRenameOpen(true); - }} - > - - - Rename - - - - { - e.stopPropagation(); - toggleVisibility(); - }} - > - {did.visible ? ( - - ) : ( - - )} - {did.visible ? t`Hide` : t`Show`} - - - - - - -
{did.launcher_id}
-
-
- - !open && setRenameOpen(false)} - > - - - - Rename Profile - - - Enter the new display name for this profile. - - -
-
- - setName(event.target.value)} - onKeyDown={(event) => { - if (event.key === 'Enter') { - event.preventDefault(); - rename(); - } - }} - /> -
-
- - - - -
-
- - - This will send the profile to the provided address. - - - - - This will permanently delete the profile by sending it to the burn - address. - - - - - - This will modify the profile's recovery info to be compatible - with the Chia reference wallet. - - - - { - setResponse(null); - setIsTransferring(false); - setIsBurning(false); - setIsNormalizing(false); - }} - onConfirm={() => updateDids()} - additionalData={ - isTransferring && response - ? { - title: t`Transfer DID`, - content: ( - - ), - } - : isBurning && response - ? { - title: t`Burn DID`, - content: , - } - : isNormalizing && response - ? { - title: t`Normalize DID`, - content: , - } - : undefined - } - /> - - ); -} diff --git a/src/pages/Nft.tsx b/src/pages/Nft.tsx index 3f4e85df..a744de5b 100644 --- a/src/pages/Nft.tsx +++ b/src/pages/Nft.tsx @@ -1,12 +1,12 @@ import { AddressItem } from '@/components/AddressItem'; import Container from '@/components/Container'; +import { DidInfo } from '@/components/DidInfo'; import Header from '@/components/Header'; import { LabeledItem } from '@/components/LabeledItem'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useErrors } from '@/hooks/useErrors'; import spacescanLogo from '@/images/spacescan-logo-192.png'; -import { getMintGardenProfile } from '@/lib/marketplaces'; import { isAudio, isImage, isJson, isText, nftUri } from '@/lib/nftUri'; import { formatTimestamp } from '@/lib/utils'; import { t } from '@lingui/core/macro'; @@ -86,36 +86,6 @@ export default function Nft() { .catch(addError); }, [addError]); - const [minterProfile, setMinterProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); - - const [ownerProfile, setOwnerProfile] = useState<{ - encoded_id: string; - name: string; - avatar_uri: string | null; - } | null>(null); - - useEffect(() => { - if (!nft?.minter_did) { - setMinterProfile(null); - return; - } - - getMintGardenProfile(nft.minter_did).then(setMinterProfile); - }, [nft?.minter_did]); - - useEffect(() => { - if (!nft?.owner_did) { - setOwnerProfile(null); - return; - } - - getMintGardenProfile(nft.owner_did).then(setOwnerProfile); - }, [nft?.owner_did]); - return ( <>
@@ -342,53 +312,8 @@ export default function Nft() { -
- - {minterProfile && ( -
- openUrl(`https://mintgarden.io/${nft?.minter_did}`) - } - > - {minterProfile.avatar_uri && ( - {`${minterProfile.name} - )} -
{minterProfile.name}
-
- )} -
- -
- - {ownerProfile && ( -
- openUrl(`https://mintgarden.io/${nft?.owner_did}`) - } - > - {ownerProfile.avatar_uri && ( - {`${ownerProfile.name} - )} -
{ownerProfile.name}
-
- )} -
+ + (null); + + useEffect(() => { + commands + .getNetwork({}) + .then((data) => setNetwork(data.kind)) + .catch(() => { + // Network fetch failed, continue with default state + }); + }, []); + + if (!launcherId) { + return ( + <> +
+ +
+ Invalid DID ID +
+
+ + ); + } + + if (isLoading) { + return ( + <> +
+ +
+ + + + + DID Profile + + + +
+
+
+
+
+
+
+
+
+ + ); + } + + if (!did) { + return ( + <> +
+ + + + + + DID Profile + + + +
+ DID not found +
+
+
+
+ + ); + } + + return ( + <> +
+ + {/* DID Profile Display */} + + + + + DID Profile + + + +
+
+ {didAsset && ( +
+ +
+

{didAsset.name}

+
+
+ )} +
+
+ + +
+
+
+
+ +
+ {/* Left Column */} +
+ {/* Technical Information */} + + + + + Technical Information + + + + {did.coin_id && did.coin_id !== '0' && ( + + )} + + + +
+ + {/* Right Column */} +
+ {/* External Links */} + + + + + External Links + + + + + + + + +
+
+
+ + ); +}