diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1f9c0bb5..fedded29 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [ "master" ] + branches: ["master", "main"] pull_request: - branches: [ "master" ] + branches: ["master", "main"] env: CARGO_TERM_COLOR: always @@ -16,8 +16,8 @@ jobs: strategy: fail-fast: false matrix: - toolchain: [ nightly, stable, 1.85.0 ] - name: [ linux, windows, macos ] + toolchain: [nightly, stable, 1.85.0] + name: [linux, windows, macos] include: - name: linux os: ubuntu-latest diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index 39cc8e63..bc72bd80 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -2,9 +2,9 @@ name: Format code on: push: - branches: [ "master" ] + branches: ["master", "main"] pull_request: - branches: [ "master" ] + branches: ["master", "main"] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 2282f9d2..b74c2dee 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,9 +2,9 @@ name: Lint code on: push: - branches: [ "master" ] + branches: ["master", "main"] pull_request: - branches: [ "master" ] + branches: ["master", "main"] env: CARGO_TERM_COLOR: always diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fec6aff2..b0637892 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -2,9 +2,9 @@ name: Run tests on: push: - branches: [ "master" ] + branches: ["master", "main"] pull_request: - branches: [ "master" ] + branches: ["master", "main"] env: CARGO_TERM_COLOR: always diff --git a/.gitignore b/.gitignore index 990e0adb..404d2f17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ /tmp # will have compiled files and executables -/target +target/ # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index a4a2436f..798d8467 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,17 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -961,6 +972,18 @@ dependencies = [ "serde", ] +[[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 = "blake3" version = "1.8.2" @@ -1020,6 +1043,29 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "bp-consensus" version = "0.11.1-alpha.2+unreviewed" @@ -1174,6 +1220,28 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[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" @@ -2056,6 +2124,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[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.31" @@ -2272,6 +2346,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" @@ -2285,7 +2362,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", "serde", ] @@ -3101,6 +3178,14 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "async-std", + "sea-orm-migration", +] + [[package]] name = "mime" version = "0.3.17" @@ -3681,6 +3766,15 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.4", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -3775,6 +3869,26 @@ dependencies = [ "strict_encoding", ] +[[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 = "quinn" version = "0.11.8" @@ -3845,6 +3959,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[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" @@ -3988,6 +4108,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[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.12.22" @@ -4177,12 +4306,15 @@ dependencies = [ "lightning-persister", "lightning-rapid-gossip-sync", "magic-crypt", + "migration", "once_cell", "rand 0.8.5", "regex", "reqwest", "rgb-lib", "scrypt", + "sea-orm", + "sea-orm-migration", "serde", "serde_json", "serial_test", @@ -4283,6 +4415,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.8" @@ -4310,8 +4471,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" dependencies = [ "arrayvec", + "borsh", + "bytes", "num-traits", + "rand 0.8.5", + "rkyv", "serde", + "serde_json", ] [[package]] @@ -4643,10 +4809,15 @@ version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64c91783d1514b99754fc6a4079081dcc2c587dadbff65c48c7f62297443536a" dependencies = [ + "bigdecimal", + "chrono", "inherent", "ordered-float", + "rust_decimal", "sea-query-derive", "serde_json", + "time", + "uuid", ] [[package]] @@ -4655,9 +4826,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", "sea-query", "serde_json", "sqlx", + "time", + "uuid", ] [[package]] @@ -4699,6 +4875,12 @@ dependencies = [ "syn 2.0.104", ] +[[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" @@ -4999,6 +5181,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "single_use_seals" version = "0.11.1-alpha.2" @@ -5134,7 +5322,9 @@ dependencies = [ "async-io 1.13.0", "async-std", "base64 0.22.1", + "bigdecimal", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -5150,14 +5340,19 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", + "rust_decimal", "rustls 0.23.29", "serde", "serde_json", "sha2 0.10.9", "smallvec", "thiserror 2.0.12", + "time", + "tokio", + "tokio-stream", "tracing", "url", + "uuid", "webpki-roots 0.26.11", ] @@ -5196,6 +5391,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.104", + "tokio", "url", ] @@ -5207,9 +5403,11 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", + "bigdecimal", "bitflags 2.9.1", "byteorder", "bytes", + "chrono", "crc", "digest 0.10.7", "dotenvy", @@ -5230,6 +5428,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "rsa", + "rust_decimal", "serde", "sha1", "sha2 0.10.9", @@ -5237,7 +5436,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", + "uuid", "whoami", ] @@ -5249,8 +5450,10 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", + "bigdecimal", "bitflags 2.9.1", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -5265,8 +5468,10 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "rand 0.8.5", + "rust_decimal", "serde", "serde_json", "sha2 0.10.9", @@ -5274,7 +5479,9 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror 2.0.12", + "time", "tracing", + "uuid", "whoami", ] @@ -5285,6 +5492,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -5298,8 +5506,10 @@ dependencies = [ "serde_urlencoded", "sqlx-core", "thiserror 2.0.12", + "time", "tracing", "url", + "uuid", ] [[package]] @@ -5446,6 +5656,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.20.0" @@ -5634,6 +5850,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.15" @@ -5655,8 +5882,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -5668,6 +5895,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -5677,11 +5913,32 @@ dependencies = [ "indexmap 2.10.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" +dependencies = [ + "indexmap 2.10.0", + "toml_datetime 0.7.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -5928,7 +6185,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] @@ -6497,6 +6756,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index d62f8055..eedcfa2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,9 @@ typenum = "1.17.0" uuid = { version = "1.11.0", default-features = false, features = ["v4"] } walkdir = "2.5.0" zip = { version = "2.2.0", default-features = false, features = ["time", "zstd"] } +sea-orm = { version = "1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] } +sea-orm-migration = { version = "1", features = ["sqlx-sqlite", "runtime-tokio-rustls"] } +migration = { path = "migration" } [dev-dependencies] dircmp = "0.2.0" diff --git a/migration/Cargo.lock b/migration/Cargo.lock new file mode 100644 index 00000000..dcc7f479 --- /dev/null +++ b/migration/Cargo.lock @@ -0,0 +1,2433 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", + "tokio", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel 2.5.0", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "inherent" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "async-std", + "sea-orm-migration", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "version_check", + "yansi", +] + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sea-bae" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" +dependencies = [ + "heck 0.4.1", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "sea-orm" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d945f62558fac19e5988680d2fdf747b734c2dbc6ce2cb81ba33ed8dde5b103" +dependencies = [ + "async-stream", + "async-trait", + "derive_more", + "futures-util", + "log", + "ouroboros", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "sqlx", + "strum", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "sea-orm-cli" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94492e2ab6c045b4cc38013809ce255d14c3d352c9f0d11e6b920e2adc948ad" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "sqlx", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c2e64a50a9cc8339f10a27577e10062c7f995488e469f2c95762c5ee847832" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.114", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7315c0cadb7e60fb17ee2bb282aa27d01911fc2a7e5836ec1d4ac37d19250bb4" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" +dependencies = [ + "inherent", + "ordered-float", + "sea-query-derive", +] + +[[package]] +name = "sea-query-binder" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" +dependencies = [ + "sea-query", + "sqlx", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" +dependencies = [ + "darling", + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.114", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2239ff574c04858ca77485f112afea1a15e53135d3097d0c86509cef1def1338" +dependencies = [ + "futures", + "sea-query", + "sea-query-binder", + "sea-schema-derive", + "sqlx", +] + +[[package]] +name = "sea-schema-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-sqlite", + "syn 2.0.114", + "tokio", + "url", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.5", +] + +[[package]] +name = "webpki-roots" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.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.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2" diff --git a/migration/Cargo.toml b/migration/Cargo.toml new file mode 100644 index 00000000..aca31dbb --- /dev/null +++ b/migration/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "1.1.0" +features = [ + "runtime-tokio-rustls", + "sqlx-sqlite", +] diff --git a/migration/README.md b/migration/README.md new file mode 100644 index 00000000..3b438d89 --- /dev/null +++ b/migration/README.md @@ -0,0 +1,41 @@ +# Running Migrator CLI + +- Generate a new migration file + ```sh + cargo run -- generate MIGRATION_NAME + ``` +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/migration/src/lib.rs b/migration/src/lib.rs new file mode 100644 index 00000000..15f7dbc7 --- /dev/null +++ b/migration/src/lib.rs @@ -0,0 +1,22 @@ +pub use sea_orm_migration::prelude::*; + +mod m20260119_080116_create_mnemonics_table; +mod m20260119_120035_create_channel_peer_data_table; +mod m20260121_120000_create_rgb_config_table; +mod m20260126_120000_create_revoked_token_table; +mod m20260126_130000_create_channel_ids_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m20260119_080116_create_mnemonics_table::Migration), + Box::new(m20260119_120035_create_channel_peer_data_table::Migration), + Box::new(m20260121_120000_create_rgb_config_table::Migration), + Box::new(m20260126_120000_create_revoked_token_table::Migration), + Box::new(m20260126_130000_create_channel_ids_table::Migration), + ] + } +} diff --git a/migration/src/m20260119_080116_create_mnemonics_table.rs b/migration/src/m20260119_080116_create_mnemonics_table.rs new file mode 100644 index 00000000..394eef11 --- /dev/null +++ b/migration/src/m20260119_080116_create_mnemonics_table.rs @@ -0,0 +1,33 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Mnemonic::Table) + .if_not_exists() + .col(pk_auto(Mnemonic::Id)) + .col(text(Mnemonic::EncryptedMnemonic)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Mnemonic::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum Mnemonic { + Table, + Id, + EncryptedMnemonic, +} diff --git a/migration/src/m20260119_120035_create_channel_peer_data_table.rs b/migration/src/m20260119_120035_create_channel_peer_data_table.rs new file mode 100644 index 00000000..7a9ef03a --- /dev/null +++ b/migration/src/m20260119_120035_create_channel_peer_data_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ChannelPeerData::Table) + .if_not_exists() + .col(pk_auto(ChannelPeerData::Id)) + .col(string(ChannelPeerData::PublicKey)) + .col(string(ChannelPeerData::SocketAddr)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(ChannelPeerData::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum ChannelPeerData { + Table, + Id, + PublicKey, + SocketAddr, +} diff --git a/migration/src/m20260121_120000_create_rgb_config_table.rs b/migration/src/m20260121_120000_create_rgb_config_table.rs new file mode 100644 index 00000000..c1b3a53d --- /dev/null +++ b/migration/src/m20260121_120000_create_rgb_config_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(RgbConfig::Table) + .if_not_exists() + .col(pk_auto(RgbConfig::Id)) + .col(string(RgbConfig::Key).unique_key()) + .col(text(RgbConfig::Value)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(RgbConfig::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum RgbConfig { + Table, + Id, + Key, + Value, +} diff --git a/migration/src/m20260126_120000_create_revoked_token_table.rs b/migration/src/m20260126_120000_create_revoked_token_table.rs new file mode 100644 index 00000000..8c659de4 --- /dev/null +++ b/migration/src/m20260126_120000_create_revoked_token_table.rs @@ -0,0 +1,33 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(RevokedToken::Table) + .if_not_exists() + .col(pk_auto(RevokedToken::Id)) + .col(string(RevokedToken::RevocationId).unique_key()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(RevokedToken::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum RevokedToken { + Table, + Id, + RevocationId, +} diff --git a/migration/src/m20260126_130000_create_channel_ids_table.rs b/migration/src/m20260126_130000_create_channel_ids_table.rs new file mode 100644 index 00000000..fd3da3a1 --- /dev/null +++ b/migration/src/m20260126_130000_create_channel_ids_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(ChannelIds::Table) + .if_not_exists() + .col(pk_auto(ChannelIds::Id)) + .col(string(ChannelIds::TemporaryChannelId).unique_key()) + .col(string(ChannelIds::ChannelId)) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(ChannelIds::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum ChannelIds { + Table, + Id, + TemporaryChannelId, + ChannelId, +} diff --git a/migration/src/main.rs b/migration/src/main.rs new file mode 100644 index 00000000..c6b6e48d --- /dev/null +++ b/migration/src/main.rs @@ -0,0 +1,6 @@ +use sea_orm_migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(migration::Migrator).await; +} diff --git a/src/auth.rs b/src/auth.rs index 42ef7a07..336f2f62 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -6,22 +6,13 @@ use axum::{ response::Response, }; use biscuit_auth::{macros::authorizer, Biscuit, PublicKey}; -use std::{ - collections::HashSet, - fs, - io::{BufRead, BufReader, Write as IoWrite}, - path::PathBuf, - sync::Arc, -}; -use tempfile::NamedTempFile; +use std::{collections::HashSet, sync::Arc}; use crate::{ error::{APIError, AppError}, utils::{hex_str, hex_str_to_vec, AppState}, }; -const REVOKED_TOKENS_FILE: &str = "revoked_tokens.txt"; - const READ_ONLY_OPS: [&str; 23] = [ "/assetbalance", "/assetmetadata", @@ -187,57 +178,25 @@ fn is_token_expired(token: &Biscuit) -> bool { } impl AppState { - pub(crate) fn revoke_token(&self, token_to_revoke: &Biscuit) -> Result<(), APIError> { + /// Revokes a token by storing its revocation identifiers in the database. + /// Also updates the in-memory cache. + pub(crate) async fn revoke_token(&self, token_to_revoke: &Biscuit) -> Result<(), APIError> { let revocation_ids = token_to_revoke.revocation_identifiers(); + let database_manager = &self.static_state.database_manager; - let file_body = { + // Store each revocation ID in the database and in-memory cache + { let mut revoked = self.revoked_tokens.lock().unwrap(); - for id in revocation_ids { - revoked.insert(id); + for id in &revocation_ids { + revoked.insert(id.clone()); } + } // drop lock before async operations - let mut updated_list = String::new(); - for token_id in revoked.iter() { - updated_list.push_str(&hex_str(token_id)); - updated_list.push('\n'); - } - updated_list - }; // drop lock - - let path = self.get_revoked_tokens_path(); - - // write to a temp file - let dir = path.parent().expect("parent defined"); - let mut tmp = NamedTempFile::new_in(dir).map_err(|e| { - tracing::error!( - "Failed to create temporary file in {}: {}", - dir.display(), - e - ); - APIError::IO(e) - })?; - tmp.as_file_mut() - .write_all(file_body.as_bytes()) - .and_then(|_| tmp.as_file_mut().flush()) - .and_then(|_| tmp.as_file().sync_all()) - .map_err(|e| { - tracing::error!( - "Failed to write/flush/sync temporary revoked-tokens file: {}", - e - ); - APIError::IO(e) - })?; - - // atomically replace the destination file with the synced temp file - tmp.persist(&path).map_err(|persist_err| { - let e = persist_err.error; - tracing::error!( - "Failed to persist temporary file to {}: {}", - path.display(), - e - ); - APIError::IO(e) - })?; + // Persist to database + for id in revocation_ids { + let hex_id = hex_str(&id); + database_manager.save_revoked_token(&hex_id).await?; + } Ok(()) } @@ -247,65 +206,4 @@ impl AppState { let revoked = self.revoked_tokens.lock().unwrap(); !revocation_ids.is_disjoint(&*revoked) } - - fn get_revoked_tokens_path(&self) -> PathBuf { - self.static_state.storage_dir_path.join(REVOKED_TOKENS_FILE) - } - - pub(crate) fn load_revoked_tokens(&self) -> Result>, AppError> { - let path = self.get_revoked_tokens_path(); - - let file = match fs::File::open(&path) { - Ok(f) => f, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - tracing::info!( - "No revoked tokens file found at {}, starting with empty set", - path.display() - ); - return Ok(HashSet::new()); - } - Err(e) => { - tracing::error!( - "Failed to open revoked tokens file {}: {}", - path.display(), - e - ); - return Err(AppError::IO(e)); - } - }; - - let mut revoked: HashSet> = HashSet::new(); - let reader = BufReader::new(file); - for (lineno, line_res) in reader.lines().enumerate() { - let line = line_res.map_err(|e| { - tracing::error!( - "I/O error while reading {} at line {}: {}", - path.display(), - lineno + 1, - e - ); - AppError::IO(e) - })?; - let s = line.trim(); - if s.is_empty() || s.starts_with('#') { - continue; - } - match hex_str_to_vec(s) { - Some(token_id) => { - revoked.insert(token_id); - } - None => { - tracing::error!( - "Invalid hex string in revoked tokens at {}:{} -> {:?}", - path.display(), - lineno + 1, - s - ); - return Err(AppError::InvalidRevokedTokensFile); - } - } - } - tracing::info!("Loaded {} revoked tokens", revoked.len()); - Ok(revoked) - } } diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 00000000..d869476b --- /dev/null +++ b/src/database.rs @@ -0,0 +1,646 @@ +use crate::entities::{channel_ids, channel_peer_data, mnemonic, prelude::*, revoked_token, rgb_config}; +use crate::utils::{hex_str, hex_str_to_vec}; +use lightning::ln::types::ChannelId; +use crate::error::APIError; +use bitcoin::secp256k1::PublicKey; +use migration::MigratorTrait; +use sea_orm::{ + ActiveModelTrait, ActiveValue, ColumnTrait, ConnectOptions, Database, DatabaseConnection, + DeleteResult, EntityTrait, QueryFilter, +}; +use std::collections::HashMap; +use std::fs; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::Mutex; + +pub struct DatabaseManager { + db: DatabaseConnection, + // Cache for RGB config to reduce database hits on frequent operations + rgb_config_cache: Arc>>>, +} + +impl DatabaseManager { + pub async fn new(db_path: &Path) -> Result { + tracing::info!("Initializing database at path: {}", db_path.display()); + let db_url = format!("sqlite://{}?mode=rwc", db_path.display()); + tracing::info!("Connecting to database URL: {}", db_url); + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(10) + .connect_timeout(Duration::from_secs(30)); + let db = Database::connect(opt) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + tracing::info!("Database connected successfully"); + + tracing::info!("Running migrations"); + migration::Migrator::up(&db, None) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + tracing::info!("Migrations completed"); + + Ok(Self { + db, + rgb_config_cache: Arc::new(Mutex::new(HashMap::new())), + }) + } + + pub async fn save_mnemonic(&self, encrypted_mnemonic: String) -> Result<(), APIError> { + tracing::info!("Saving mnemonic to database"); + Mnemonic::delete_many() + .exec(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + let new_mnemonic = mnemonic::ActiveModel { + id: ActiveValue::NotSet, + encrypted_mnemonic: ActiveValue::Set(encrypted_mnemonic), + }; + + new_mnemonic + .insert(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + tracing::info!("Mnemonic saved successfully"); + + Ok(()) + } + + pub async fn load_mnemonic(&self) -> Result { + tracing::info!("Loading mnemonic from database"); + let mnemonic_model = Mnemonic::find() + .one(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))? + .ok_or(APIError::NotInitialized)?; + tracing::info!("Mnemonic loaded successfully"); + + Ok(mnemonic_model.encrypted_mnemonic) + } + + pub async fn check_already_initialized(&self) -> Result { + tracing::info!("Checking if already initialized"); + let mnemonics = Mnemonic::find() + .all(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + let initialized = !mnemonics.is_empty(); + tracing::info!("Already initialized: {}", initialized); + Ok(initialized) + } + + pub async fn save_channel_peer( + &self, + pubkey: &PublicKey, + address: &SocketAddr, + ) -> Result<(), APIError> { + tracing::info!("Saving channel peer to database: {}@{}", pubkey, address); + + // Delete existing entry for this pubkey if it exists + ChannelPeerData::delete_many() + .filter(channel_peer_data::Column::PublicKey.eq(pubkey.to_string())) + .exec(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + // Insert new entry + let new_peer = channel_peer_data::ActiveModel { + id: ActiveValue::NotSet, + public_key: ActiveValue::Set(pubkey.to_string()), + socket_addr: ActiveValue::Set(address.to_string()), + }; + + new_peer + .insert(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + tracing::info!("Channel peer saved successfully"); + Ok(()) + } + + pub async fn load_channel_peers(&self) -> Result, APIError> { + tracing::debug!("Loading channel peers from database"); + + let peers = ChannelPeerData::find() + .all(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + let mut peer_data = HashMap::new(); + for peer in peers { + let pubkey: PublicKey = peer + .public_key + .parse() + .map_err(|e| APIError::InvalidPeerInfo(format!("Invalid public key: {}", e)))?; + let socket_addr: SocketAddr = peer + .socket_addr + .parse() + .map_err(|e| APIError::InvalidPeerInfo(format!("Invalid socket address: {}", e)))?; + peer_data.insert(pubkey, socket_addr); + } + + tracing::debug!("Loaded {} channel peers from database", peer_data.len()); + Ok(peer_data) + } + + pub async fn delete_channel_peer(&self, pubkey: &PublicKey) -> Result<(), APIError> { + tracing::info!("Deleting channel peer from database: {}", pubkey); + + let result: DeleteResult = ChannelPeerData::delete_many() + .filter(channel_peer_data::Column::PublicKey.eq(pubkey.to_string())) + .exec(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + if result.rows_affected > 0 { + tracing::info!("Channel peer deleted successfully"); + } else { + tracing::warn!("Channel peer not found for deletion: {}", pubkey); + } + + Ok(()) + } + + pub async fn save_rgb_config(&self, key: &str, value: &str) -> Result<(), APIError> { + tracing::info!("Saving RGB config to database: {} = {}", key, value); + + let existing = RgbConfig::find() + .filter(rgb_config::Column::Key.eq(key)) + .one(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + if let Some(model) = existing { + let mut active_model: rgb_config::ActiveModel = model.into(); + active_model.value = ActiveValue::Set(value.to_string()); + active_model + .update(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + } else { + let new_config = rgb_config::ActiveModel { + id: ActiveValue::NotSet, + key: ActiveValue::Set(key.to_string()), + value: ActiveValue::Set(value.to_string()), + }; + new_config + .insert(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + } + + // Update cache with new value + self.rgb_config_cache + .lock() + .await + .insert(key.to_string(), Some(value.to_string())); + + tracing::info!("RGB config saved successfully"); + Ok(()) + } + + pub async fn load_rgb_config(&self, key: &str) -> Result, APIError> { + tracing::debug!("Loading RGB config from cache/database: {}", key); + + // Check cache first + { + let cache = self.rgb_config_cache.lock().await; + if let Some(cached_value) = cache.get(key) { + return Ok(cached_value.clone()); + } + } + + // Load from database and cache it + let config = RgbConfig::find() + .filter(rgb_config::Column::Key.eq(key)) + .one(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + let value = config.map(|c| c.value); + { + let mut cache = self.rgb_config_cache.lock().await; + cache.insert(key.to_string(), value.clone()); + } + + Ok(value) + } + + pub async fn migrate_indexer_url_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const INDEXER_URL_FNAME: &str = "indexer_url"; + + let indexer_url_path = storage_dir.join(INDEXER_URL_FNAME); + + if !indexer_url_path.exists() { + tracing::info!("No existing indexer_url file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing indexer_url file, migrating to database"); + + let indexer_url = fs::read_to_string(&indexer_url_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("indexer_url", &indexer_url).await?; + + tracing::info!("Successfully migrated indexer_url from file to database"); + + Ok(()) + } + + pub async fn migrate_bitcoin_network_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const BITCOIN_NETWORK_FNAME: &str = "bitcoin_network"; + + let bitcoin_network_path = storage_dir.join(BITCOIN_NETWORK_FNAME); + + if !bitcoin_network_path.exists() { + tracing::info!("No existing bitcoin_network file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing bitcoin_network file, migrating to database"); + + let bitcoin_network = fs::read_to_string(&bitcoin_network_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("bitcoin_network", &bitcoin_network).await?; + + tracing::info!("Successfully migrated bitcoin_network from file to database"); + + Ok(()) + } + + pub async fn migrate_wallet_fingerprint_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const WALLET_FINGERPRINT_FNAME: &str = "wallet_fingerprint"; + + let wallet_fingerprint_path = storage_dir.join(WALLET_FINGERPRINT_FNAME); + + if !wallet_fingerprint_path.exists() { + tracing::info!("No existing wallet_fingerprint file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing wallet_fingerprint file, migrating to database"); + + let wallet_fingerprint = fs::read_to_string(&wallet_fingerprint_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("wallet_fingerprint", &wallet_fingerprint).await?; + + tracing::info!("Successfully migrated wallet_fingerprint from file to database"); + + Ok(()) + } + + pub async fn migrate_wallet_account_xpub_colored_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const WALLET_ACCOUNT_XPUB_COLORED_FNAME: &str = "wallet_account_xpub_colored"; + + let wallet_account_xpub_colored_path = storage_dir.join(WALLET_ACCOUNT_XPUB_COLORED_FNAME); + + if !wallet_account_xpub_colored_path.exists() { + tracing::info!("No existing wallet_account_xpub_colored file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing wallet_account_xpub_colored file, migrating to database"); + + let wallet_account_xpub_colored = fs::read_to_string(&wallet_account_xpub_colored_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("wallet_account_xpub_colored", &wallet_account_xpub_colored).await?; + + tracing::info!("Successfully migrated wallet_account_xpub_colored from file to database"); + + Ok(()) + } + + pub async fn migrate_wallet_account_xpub_vanilla_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const WALLET_ACCOUNT_XPUB_VANILLA_FNAME: &str = "wallet_account_xpub_vanilla"; + + let wallet_account_xpub_vanilla_path = storage_dir.join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME); + + if !wallet_account_xpub_vanilla_path.exists() { + tracing::info!("No existing wallet_account_xpub_vanilla file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing wallet_account_xpub_vanilla file, migrating to database"); + + let wallet_account_xpub_vanilla = fs::read_to_string(&wallet_account_xpub_vanilla_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("wallet_account_xpub_vanilla", &wallet_account_xpub_vanilla).await?; + + tracing::info!("Successfully migrated wallet_account_xpub_vanilla from file to database"); + + Ok(()) + } + + pub async fn migrate_wallet_master_fingerprint_from_file(&self, storage_dir: &Path) -> Result<(), APIError> { + const WALLET_MASTER_FINGERPRINT_FNAME: &str = "wallet_master_fingerprint"; + + let wallet_master_fingerprint_path = storage_dir.join(WALLET_MASTER_FINGERPRINT_FNAME); + + if !wallet_master_fingerprint_path.exists() { + tracing::info!("No existing wallet_master_fingerprint file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing wallet_master_fingerprint file, migrating to database"); + + let wallet_master_fingerprint = fs::read_to_string(&wallet_master_fingerprint_path) + .map_err(APIError::IO)? + .trim() + .to_string(); + + self.save_rgb_config("wallet_master_fingerprint", &wallet_master_fingerprint).await?; + + tracing::info!("Successfully migrated wallet_master_fingerprint from file to database"); + + Ok(()) + } + + /// Saves a revoked token's revocation identifier to the database. + /// The revocation_id is stored as a hex-encoded string. + pub async fn save_revoked_token(&self, revocation_id_hex: &str) -> Result<(), APIError> { + tracing::debug!("Saving revoked token to database: {}", revocation_id_hex); + + // Check if already exists to avoid duplicate key errors + let existing = RevokedToken::find() + .filter(revoked_token::Column::RevocationId.eq(revocation_id_hex)) + .one(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + if existing.is_some() { + tracing::debug!("Revocation ID already exists in database, skipping"); + return Ok(()); + } + + let new_revoked_token = revoked_token::ActiveModel { + id: ActiveValue::NotSet, + revocation_id: ActiveValue::Set(revocation_id_hex.to_string()), + }; + + new_revoked_token + .insert(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + tracing::debug!("Revoked token saved successfully"); + Ok(()) + } + + /// Loads all revoked token revocation identifiers from the database. + /// Returns a HashSet of raw byte vectors (decoded from hex). + pub async fn load_revoked_tokens(&self) -> Result>, APIError> { + tracing::info!("Loading revoked tokens from database"); + + let tokens = RevokedToken::find() + .all(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + let mut revoked: std::collections::HashSet> = std::collections::HashSet::new(); + for token in tokens { + if let Some(bytes) = crate::utils::hex_str_to_vec(&token.revocation_id) { + revoked.insert(bytes); + } else { + tracing::warn!( + "Invalid hex string in revoked_token table: {}", + token.revocation_id + ); + } + } + + tracing::info!("Loaded {} revoked tokens from database", revoked.len()); + Ok(revoked) + } + + pub async fn save_channel_id( + &self, + temporary_channel_id: &ChannelId, + channel_id: &ChannelId, + ) -> Result<(), APIError> { + let temp_id_hex = hex_str(&temporary_channel_id.0); + let chan_id_hex = hex_str(&channel_id.0); + tracing::debug!( + "Saving channel ID mapping to database: {} -> {}", + temp_id_hex, + chan_id_hex + ); + + let existing = ChannelIds::find() + .filter(channel_ids::Column::TemporaryChannelId.eq(&temp_id_hex)) + .one(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + if let Some(model) = existing { + let mut active_model: channel_ids::ActiveModel = model.into(); + active_model.channel_id = ActiveValue::Set(chan_id_hex); + active_model + .update(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + } else { + let new_entry = channel_ids::ActiveModel { + id: ActiveValue::NotSet, + temporary_channel_id: ActiveValue::Set(temp_id_hex), + channel_id: ActiveValue::Set(chan_id_hex), + }; + new_entry + .insert(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + } + + tracing::debug!("Channel ID mapping saved successfully"); + Ok(()) + } + + pub async fn load_channel_ids(&self) -> Result, APIError> { + tracing::debug!("Loading channel IDs from database"); + + let entries = ChannelIds::find() + .all(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + let mut channel_ids_map = HashMap::new(); + for entry in entries { + let temp_id_bytes = match hex_str_to_vec(&entry.temporary_channel_id) { + Some(bytes) => bytes, + None => { + tracing::warn!( + "Invalid temporary_channel_id hex in database: {}", + entry.temporary_channel_id + ); + continue; + } + }; + let chan_id_bytes = match hex_str_to_vec(&entry.channel_id) { + Some(bytes) => bytes, + None => { + tracing::warn!( + "Invalid channel_id hex in database: {}", + entry.channel_id + ); + continue; + } + }; + + if temp_id_bytes.len() != 32 || chan_id_bytes.len() != 32 { + tracing::warn!( + "Invalid channel ID length in database: temp={}, chan={}", + temp_id_bytes.len(), + chan_id_bytes.len() + ); + continue; + } + + let temp_id = ChannelId(<[u8; 32]>::try_from(temp_id_bytes.as_slice()).unwrap()); + let chan_id = ChannelId(<[u8; 32]>::try_from(chan_id_bytes.as_slice()).unwrap()); + channel_ids_map.insert(temp_id, chan_id); + } + + tracing::debug!("Loaded {} channel ID mappings from database", channel_ids_map.len()); + Ok(channel_ids_map) + } + + pub async fn delete_channel_id_by_channel_id( + &self, + channel_id: &ChannelId, + ) -> Result<(), APIError> { + let chan_id_hex = hex_str(&channel_id.0); + tracing::debug!("Deleting channel ID mapping by channel_id: {}", chan_id_hex); + + let result: DeleteResult = ChannelIds::delete_many() + .filter(channel_ids::Column::ChannelId.eq(&chan_id_hex)) + .exec(&self.db) + .await + .map_err(|e| APIError::DatabaseError(e.to_string()))?; + + if result.rows_affected > 0 { + tracing::debug!("Channel ID mapping deleted successfully"); + } else { + tracing::debug!("No channel ID mapping found for deletion"); + } + + Ok(()) + } + + pub async fn migrate_channel_ids_from_file( + &self, + ldk_data_dir: &Path, + ) -> Result<(), APIError> { + use crate::disk::{read_channel_ids_info, CHANNEL_IDS_FNAME}; + + let channel_ids_path = ldk_data_dir.join(CHANNEL_IDS_FNAME); + + if !channel_ids_path.exists() { + tracing::info!("No existing channel_ids file found, skipping migration"); + return Ok(()); + } + + tracing::info!("Found existing channel_ids file, migrating to database"); + + let channel_ids_map = read_channel_ids_info(&channel_ids_path); + + for (temp_id, chan_id) in channel_ids_map.channel_ids.iter() { + self.save_channel_id(temp_id, chan_id).await?; + } + + tracing::info!( + "Successfully migrated {} channel ID mappings from file to database", + channel_ids_map.channel_ids.len() + ); + + if let Err(e) = fs::remove_file(&channel_ids_path) { + tracing::warn!("Failed to remove old channel_ids file: {}", e); + } else { + tracing::info!("Removed old channel_ids file after migration"); + } + + Ok(()) + } + + /// Syncs RGB config values from database to filesystem files. + /// This is necessary because the rust-lightning library reads these values directly from files + /// during RGB wallet operations (e.g., _get_indexer_url, _accept_transfer). + /// The database is the source of truth, but files serve as a read-only cache for library compatibility. + pub async fn sync_rgb_config_to_files(&self, storage_dir: &Path) -> Result<(), APIError> { + const INDEXER_URL_FNAME: &str = "indexer_url"; + const PROXY_ENDPOINT_FNAME: &str = "proxy_endpoint"; + const BITCOIN_NETWORK_FNAME: &str = "bitcoin_network"; + const WALLET_FINGERPRINT_FNAME: &str = "wallet_fingerprint"; + const WALLET_ACCOUNT_XPUB_COLORED_FNAME: &str = "wallet_account_xpub_colored"; + const WALLET_ACCOUNT_XPUB_VANILLA_FNAME: &str = "wallet_account_xpub_vanilla"; + const WALLET_MASTER_FINGERPRINT_FNAME: &str = "wallet_master_fingerprint"; + + let indexer_url = self.load_rgb_config("indexer_url").await?; + let proxy_endpoint = self.load_rgb_config("proxy_endpoint").await?; + let bitcoin_network = self.load_rgb_config("bitcoin_network").await?; + let wallet_fingerprint = self.load_rgb_config("wallet_fingerprint").await?; + let wallet_account_xpub_colored = self.load_rgb_config("wallet_account_xpub_colored").await?; + let wallet_account_xpub_vanilla = self.load_rgb_config("wallet_account_xpub_vanilla").await?; + let wallet_master_fingerprint = self.load_rgb_config("wallet_master_fingerprint").await?; + + if let Some(url) = indexer_url { + let indexer_url_path = storage_dir.join(INDEXER_URL_FNAME); + fs::write(&indexer_url_path, url).map_err(APIError::IO)?; + tracing::info!("Synced indexer_url to file"); + } + + if let Some(proxy) = proxy_endpoint { + let proxy_endpoint_path = storage_dir.join(PROXY_ENDPOINT_FNAME); + fs::write(&proxy_endpoint_path, proxy).map_err(APIError::IO)?; + tracing::info!("Synced proxy_endpoint to file"); + } + + if let Some(network) = bitcoin_network { + let bitcoin_network_path = storage_dir.join(BITCOIN_NETWORK_FNAME); + fs::write(&bitcoin_network_path, network).map_err(APIError::IO)?; + tracing::info!("Synced bitcoin_network to file"); + } + + if let Some(fingerprint) = wallet_fingerprint { + let wallet_fingerprint_path = storage_dir.join(WALLET_FINGERPRINT_FNAME); + fs::write(&wallet_fingerprint_path, fingerprint).map_err(APIError::IO)?; + tracing::info!("Synced wallet_fingerprint to file"); + } + + if let Some(xpub_colored) = wallet_account_xpub_colored { + let wallet_account_xpub_colored_path = storage_dir.join(WALLET_ACCOUNT_XPUB_COLORED_FNAME); + fs::write(&wallet_account_xpub_colored_path, xpub_colored).map_err(APIError::IO)?; + tracing::info!("Synced wallet_account_xpub_colored to file"); + } + + if let Some(xpub_vanilla) = wallet_account_xpub_vanilla { + let wallet_account_xpub_vanilla_path = storage_dir.join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME); + fs::write(&wallet_account_xpub_vanilla_path, xpub_vanilla).map_err(APIError::IO)?; + tracing::info!("Synced wallet_account_xpub_vanilla to file"); + } + + if let Some(master_fingerprint) = wallet_master_fingerprint { + let wallet_master_fingerprint_path = storage_dir.join(WALLET_MASTER_FINGERPRINT_FNAME); + fs::write(&wallet_master_fingerprint_path, master_fingerprint).map_err(APIError::IO)?; + tracing::info!("Synced wallet_master_fingerprint to file"); + } + + Ok(()) + } +} diff --git a/src/disk.rs b/src/disk.rs index 6c6f104e..d917b236 100644 --- a/src/disk.rs +++ b/src/disk.rs @@ -1,32 +1,26 @@ -use bitcoin::secp256k1::PublicKey; use bitcoin::Network; use chrono::Utc; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters}; use lightning::util::hash_tables::new_hash_map; use lightning::util::logger::{Logger, Record}; use lightning::util::ser::{Readable, ReadableArgs, Writer}; -use std::collections::HashMap; use std::fs; use std::fs::File; -use std::io::{BufRead, BufReader}; -use std::net::SocketAddr; +use std::io::BufReader; use std::path::{Path, PathBuf}; use std::sync::Arc; -use crate::error::APIError; use crate::ldk::{ ChannelIdsMap, InboundPaymentInfoStorage, NetworkGraph, OutboundPaymentInfoStorage, OutputSpenderTxes, SwapMap, }; -use crate::utils::{parse_peer_info, LOGS_DIR}; +use crate::utils::LOGS_DIR; pub(crate) const LDK_LOGS_FILE: &str = "logs.txt"; pub(crate) const INBOUND_PAYMENTS_FNAME: &str = "inbound_payments"; pub(crate) const OUTBOUND_PAYMENTS_FNAME: &str = "outbound_payments"; -pub(crate) const CHANNEL_PEER_DATA: &str = "channel_peer_data"; - pub(crate) const OUTPUT_SPENDER_TXES: &str = "output_spender_txes"; pub(crate) const CHANNEL_IDS_FNAME: &str = "channel_ids"; @@ -73,76 +67,6 @@ impl Logger for FilesystemLogger { } } -pub(crate) fn persist_channel_peer( - path: &Path, - pubkey: &PublicKey, - address: &SocketAddr, -) -> Result<(), APIError> { - let pubkey = pubkey.to_string(); - let peer_info = if path.exists() { - let mut updated_peer_info = fs::read_to_string(path)? - .lines() - .filter(|&line| !line.trim().starts_with(&pubkey)) - .map(|line| line.trim()) - .collect::>() - .join("\n"); - updated_peer_info += format!( - "{}{pubkey}@{address}", - if updated_peer_info.is_empty() { - "" - } else { - "\n" - } - ) - .as_str(); - updated_peer_info - } else { - format!("{pubkey}@{address}") - }; - let mut tmp_path = path.to_path_buf(); - tmp_path.set_extension("ptmp"); - fs::write(&tmp_path, peer_info.to_string().as_bytes())?; - fs::rename(tmp_path, path)?; - tracing::info!("persisted peer (pubkey: {pubkey}, addr: {address})"); - Ok(()) -} - -pub(crate) fn delete_channel_peer(path: &Path, pubkey: String) -> Result<(), APIError> { - if path.exists() { - let updated_peer_info = fs::read_to_string(path)? - .lines() - .filter(|&line| !line.trim().starts_with(&pubkey)) - .map(|line| line.trim()) - .collect::>() - .join("\n"); - let mut tmp_path = path.to_path_buf(); - tmp_path.set_extension("dtmp"); - fs::write(&tmp_path, updated_peer_info.to_string().as_bytes())?; - fs::rename(tmp_path, path)?; - } - Ok(()) -} - -pub(crate) fn read_channel_peer_data( - path: &Path, -) -> Result, APIError> { - let mut peer_data = HashMap::new(); - if !path.exists() { - return Ok(HashMap::new()); - } - let file = File::open(path)?; - let reader = BufReader::new(file); - for line in reader.lines() { - match parse_peer_info(line.unwrap()) { - Ok((pubkey, socket_addr)) => { - peer_data.insert(pubkey, socket_addr.expect("saved info with address")); - } - Err(e) => return Err(e), - } - } - Ok(peer_data) -} - pub(crate) fn read_network( path: &Path, network: Network, diff --git a/src/entities/channel_ids.rs b/src/entities/channel_ids.rs new file mode 100644 index 00000000..d24816f8 --- /dev/null +++ b/src/entities/channel_ids.rs @@ -0,0 +1,19 @@ +//! `SeaORM` Entity for channel_ids table + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "channel_ids")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(unique)] + pub temporary_channel_id: String, + pub channel_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/channel_peer_data.rs b/src/entities/channel_peer_data.rs new file mode 100644 index 00000000..52215490 --- /dev/null +++ b/src/entities/channel_peer_data.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "channel_peer_data")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub public_key: String, + pub socket_addr: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/mnemonic.rs b/src/entities/mnemonic.rs new file mode 100644 index 00000000..2005cf23 --- /dev/null +++ b/src/entities/mnemonic.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "mnemonic")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_type = "Text")] + pub encrypted_mnemonic: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/mod.rs b/src/entities/mod.rs new file mode 100644 index 00000000..a5a5f236 --- /dev/null +++ b/src/entities/mod.rs @@ -0,0 +1,9 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub mod prelude; + +pub mod channel_ids; +pub mod channel_peer_data; +pub mod mnemonic; +pub mod revoked_token; +pub mod rgb_config; diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs new file mode 100644 index 00000000..05cb02ef --- /dev/null +++ b/src/entities/prelude.rs @@ -0,0 +1,7 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.19 + +pub use super::channel_ids::Entity as ChannelIds; +pub use super::channel_peer_data::Entity as ChannelPeerData; +pub use super::mnemonic::Entity as Mnemonic; +pub use super::revoked_token::Entity as RevokedToken; +pub use super::rgb_config::Entity as RgbConfig; diff --git a/src/entities/revoked_token.rs b/src/entities/revoked_token.rs new file mode 100644 index 00000000..122edcea --- /dev/null +++ b/src/entities/revoked_token.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity for revoked token storage. +//! This table stores revocation identifiers for revoked Biscuit tokens. + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "revoked_token")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub revocation_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/rgb_config.rs b/src/entities/rgb_config.rs new file mode 100644 index 00000000..52aa8319 --- /dev/null +++ b/src/entities/rgb_config.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity for RGB configuration storage. +// This table stores indexer_url and proxy_endpoint values persistently, +// serving as the source of truth while files provide compatibility with rust-lightning. + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "rgb_config")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub key: String, + #[sea_orm(column_type = "Text")] + pub value: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/error.rs b/src/error.rs index 52e04778..9951130d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -72,8 +72,8 @@ pub enum APIError { #[error("Failed to issue asset: {0}")] FailedIssuingAsset(String), - #[error("Unable to create keys seed file {0}: {1}")] - FailedKeysCreation(String, String), + #[error("Database error: {0}")] + DatabaseError(String), #[error("Failed to open channel: {0}")] FailedOpenChannel(String), @@ -400,10 +400,10 @@ impl IntoResponse for APIError { json_rejection.body_text(), self.name(), ), - APIError::FailedClosingChannel(_) + APIError::DatabaseError(_) + | APIError::FailedClosingChannel(_) | APIError::FailedInvoiceCreation(_) | APIError::FailedIssuingAsset(_) - | APIError::FailedKeysCreation(_, _) | APIError::FailedOpenChannel(_) | APIError::FailedPayment(_) | APIError::FailedPeerDisconnection(_) @@ -529,12 +529,12 @@ pub enum AppError { #[error("The provided authentication args are invalid")] InvalidAuthenticationArgs, - #[error("The revoked tokens file contains an invalid entry")] - InvalidRevokedTokensFile, - #[error("The provided root public key is invalid")] InvalidRootKey, + #[error("API error: {0}")] + Api(#[from] APIError), + #[error("IO error: {0}")] IO(#[from] std::io::Error), diff --git a/src/ldk.rs b/src/ldk.rs index 32608d42..ede71cd0 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -23,9 +23,7 @@ use lightning::onion_message::messenger::{ }; use lightning::rgb_utils::{ get_rgb_channel_info_pending, is_channel_rgb, parse_rgb_payment_info, read_rgb_transfer_info, - update_rgb_channel_amount, BITCOIN_NETWORK_FNAME, INDEXER_URL_FNAME, STATIC_BLINDING, - WALLET_ACCOUNT_XPUB_COLORED_FNAME, WALLET_ACCOUNT_XPUB_VANILLA_FNAME, WALLET_FINGERPRINT_FNAME, - WALLET_MASTER_FINGERPRINT_FNAME, + update_rgb_channel_amount, STATIC_BLINDING, }; use lightning::routing::gossip; use lightning::routing::gossip::{NodeId, P2PGossipSync}; @@ -90,11 +88,12 @@ use time::OffsetDateTime; use tokio::runtime::Handle; use tokio::sync::watch::Sender; use tokio::task::JoinHandle; +use tokio::time::timeout; use crate::bitcoind::BitcoindClient; use crate::disk::{ - self, FilesystemLogger, CHANNEL_IDS_FNAME, CHANNEL_PEER_DATA, INBOUND_PAYMENTS_FNAME, - MAKER_SWAPS_FNAME, OUTBOUND_PAYMENTS_FNAME, OUTPUT_SPENDER_TXES, TAKER_SWAPS_FNAME, + self, FilesystemLogger, INBOUND_PAYMENTS_FNAME, MAKER_SWAPS_FNAME, OUTBOUND_PAYMENTS_FNAME, + OUTPUT_SPENDER_TXES, TAKER_SWAPS_FNAME, }; use crate::error::APIError; use crate::rgb::{check_rgb_proxy_endpoint, get_rgb_channel_info_optional, RgbLibWalletWrapper}; @@ -370,36 +369,63 @@ impl UnlockedAppState { former_temporary_channel_id: ChannelId, channel_id: ChannelId, ) { - let mut channel_ids_map = self.get_channel_ids_map(); - channel_ids_map - .channel_ids - .insert(former_temporary_channel_id, channel_id); - self.save_channel_ids_map(channel_ids_map); + { + let mut channel_ids_map = self.get_channel_ids_map(); + channel_ids_map + .channel_ids + .insert(former_temporary_channel_id, channel_id); + } // Release the lock before spawning async task + + // Persist to database in a separate blocking thread with its own runtime + // This avoids deadlocks with single-threaded tokio runtimes + let db_manager = Arc::clone(&self.database_manager); + let temp_id = former_temporary_channel_id; + let chan_id = channel_id; + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create runtime for channel_id persistence"); + rt.block_on(async { + if let Err(e) = db_manager.save_channel_id(&temp_id, &chan_id).await { + tracing::error!("Failed to save channel ID to database: {}", e); + } + }); + }); } pub(crate) fn delete_channel_id(&self, channel_id: ChannelId) { - let mut channel_ids_map = self.get_channel_ids_map(); - if let Some(temporary_channel_id) = channel_ids_map - .channel_ids - .clone() - .into_iter() - .find_map(|(tmp_chan_id, chan_id)| { - if chan_id == channel_id { - Some(tmp_chan_id) - } else { - None - } - }) { - channel_ids_map.channel_ids.remove(&temporary_channel_id); - self.save_channel_ids_map(channel_ids_map); + let mut channel_ids_map = self.get_channel_ids_map(); + if let Some(temporary_channel_id) = + channel_ids_map.channel_ids.clone().into_iter().find_map( + |(tmp_chan_id, chan_id)| { + if chan_id == channel_id { + Some(tmp_chan_id) + } else { + None + } + }, + ) + { + channel_ids_map.channel_ids.remove(&temporary_channel_id); + } } - } - fn save_channel_ids_map(&self, channel_ids: MutexGuard) { - self.fs_store - .write("", "", CHANNEL_IDS_FNAME, channel_ids.encode()) - .unwrap(); + // Delete from database in a separate blocking thread with its own runtime + let db_manager = Arc::clone(&self.database_manager); + let chan_id = channel_id; + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create runtime for channel_id deletion"); + rt.block_on(async { + if let Err(e) = db_manager.delete_channel_id_by_channel_id(&chan_id).await { + tracing::error!("Failed to delete channel ID from database: {}", e); + } + }); + }); } } @@ -582,12 +608,21 @@ async fn handle_ldk_events( }]}; let unlocked_state_copy = unlocked_state.clone(); - let unsigned_psbt = tokio::task::spawn_blocking(move || { - unlocked_state_copy - .rgb_send_begin(recipient_map, true, FEE_RATE, MIN_CHANNEL_CONFIRMATIONS) - .unwrap() - }) + let unsigned_psbt = timeout( + std::time::Duration::from_secs(30), + tokio::task::spawn_blocking(move || { + unlocked_state_copy + .rgb_send_begin( + recipient_map, + true, + FEE_RATE, + MIN_CHANNEL_CONFIRMATIONS, + ) + .unwrap() + }), + ) .await + .unwrap_or_else(|_| panic!("rgb_send_begin timed out")) .unwrap(); (unsigned_psbt, Some(asset_id)) } else { @@ -597,7 +632,16 @@ async fn handle_ldk_events( (unsigned_psbt, None) }; - let signed_psbt = unlocked_state.rgb_sign_psbt(unsigned_psbt).unwrap(); + let unlocked_state_copy = unlocked_state.clone(); + let signed_psbt = timeout( + std::time::Duration::from_secs(30), + tokio::task::spawn_blocking(move || { + unlocked_state_copy.rgb_sign_psbt(unsigned_psbt).unwrap() + }), + ) + .await + .unwrap_or_else(|_| panic!("rgb_sign_psbt timed out")) + .unwrap(); let psbt = Psbt::from_str(&signed_psbt).unwrap(); let funding_tx = psbt.clone().extract_tx().unwrap(); @@ -607,39 +651,59 @@ async fn handle_ldk_events( let psbt_path = static_state .ldk_data_dir .join(format!("psbt_{funding_txid}")); - fs::write(psbt_path, psbt.to_string()).unwrap(); - - if let Some(asset_id) = asset_id { - let unlocked_state_copy = unlocked_state.clone(); - let witness_id = funding_txid.clone(); + timeout( + std::time::Duration::from_secs(10), tokio::task::spawn_blocking(move || { - unlocked_state_copy - .rgb_upsert_witness( - RgbTxid::from_str(&witness_id).unwrap(), - WitnessOrd::Tentative, - ) - .unwrap() - }) - .await - .unwrap(); + fs::write(psbt_path, psbt.to_string()).unwrap(); + }), + ) + .await + .unwrap_or_else(|_| panic!("PSBT file write timed out")) + .unwrap(); + if let Some(asset_id) = asset_id { let consignment_path = unlocked_state.rgb_get_send_consignment_path(&asset_id, &funding_txid); let proxy_url = TransportEndpoint::new(unlocked_state.proxy_endpoint.clone()) .unwrap() .endpoint; let unlocked_state_copy = unlocked_state.clone(); - let res = tokio::task::spawn_blocking(move || { - unlocked_state_copy.rgb_post_consignment( - &proxy_url, - funding_txid.clone(), - &consignment_path, - funding_txid, - None, - ) - }) - .await - .unwrap(); + let witness_id = funding_txid.clone(); + let funding_txid_clone = funding_txid.clone(); + + let rgb_result = tokio::time::timeout( + std::time::Duration::from_secs(30), + tokio::task::spawn_blocking(move || { + // rgb_upsert_witness + unlocked_state_copy + .rgb_upsert_witness( + RgbTxid::from_str(&witness_id).unwrap(), + WitnessOrd::Tentative, + ) + .unwrap(); + + unlocked_state_copy.rgb_post_consignment( + &proxy_url, + funding_txid_clone.clone(), + &consignment_path, + funding_txid_clone, + None, + ) + }), + ) + .await; + + let res = match rgb_result { + Ok(Ok(result)) => result, + Ok(Err(join_err)) => { + tracing::error!("RGB spawn_blocking task failed: {}", join_err); + return Err(ReplayEvent()); + } + Err(_) => { + tracing::error!("RGB operation timed out after 30 seconds"); + return Err(ReplayEvent()); + } + }; if let Err(e) = res { tracing::error!("cannot post consignment: {e}"); @@ -650,6 +714,10 @@ async fn handle_ldk_events( let channel_manager_copy = unlocked_state.channel_manager.clone(); // Give the funding transaction back to LDK for opening the channel. + tracing::info!( + "Calling funding_transaction_generated for channel {}", + temporary_channel_id + ); if channel_manager_copy .funding_transaction_generated( temporary_channel_id, @@ -752,7 +820,11 @@ async fn handle_ldk_events( } } - _update_rgb_channel_amount(&static_state.ldk_data_dir, &payment_hash, true); + tokio::task::spawn_blocking(move || { + _update_rgb_channel_amount(&static_state.ldk_data_dir, &payment_hash, true); + }) + .await + .unwrap(); if is_maker_swap { unlocked_state.update_maker_swap_status(&payment_hash, SwapStatus::Succeeded); } else { @@ -773,7 +845,11 @@ async fn handle_ldk_events( payment_id, .. } => { - _update_rgb_channel_amount(&static_state.ldk_data_dir, &payment_hash, false); + tokio::task::spawn_blocking(move || { + _update_rgb_channel_amount(&static_state.ldk_data_dir, &payment_hash, false); + }) + .await + .unwrap(); if unlocked_state.is_maker_swap(&payment_hash) { tracing::info!( @@ -811,7 +887,8 @@ async fn handle_ldk_events( random_bytes .copy_from_slice(&unlocked_state.keys_manager.get_secure_random_bytes()[..16]); let user_channel_id = u128::from_be_bytes(random_bytes); - let res = unlocked_state.channel_manager.accept_inbound_channel( + let channel_manager_copy = unlocked_state.channel_manager.clone(); + let res = channel_manager_copy.accept_inbound_channel( temporary_channel_id, counterparty_node_id, user_channel_id, @@ -916,71 +993,86 @@ async fn handle_ldk_events( unlocked_state.update_taker_swap_status(&payment_hash, SwapStatus::Succeeded); } - let read_only_network_graph = unlocked_state.network_graph.read_only(); - let nodes = read_only_network_graph.nodes(); - let channels = unlocked_state.channel_manager.list_channels(); + let network_graph_copy = unlocked_state.network_graph.clone(); + let channel_manager_copy = unlocked_state.channel_manager.clone(); + let prev_channel_id_copy = prev_channel_id; + let next_channel_id_copy = next_channel_id; + let claim_from_onchain_tx_copy = claim_from_onchain_tx; + let outbound_amount_forwarded_msat_copy = outbound_amount_forwarded_msat; + let total_fee_earned_msat_copy = total_fee_earned_msat; + + tokio::task::spawn_blocking(move || { + let read_only_network_graph = network_graph_copy.read_only(); + let nodes = read_only_network_graph.nodes(); + let channels = channel_manager_copy.list_channels(); - let node_str = |channel_id: &Option| match channel_id { - None => String::new(), - Some(channel_id) => match channels.iter().find(|c| c.channel_id == *channel_id) { + let node_str = |channel_id: &Option| match channel_id { None => String::new(), - Some(channel) => { - match nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) { - None => "private node".to_string(), - Some(node) => match &node.announcement_info { - None => "unnamed node".to_string(), - Some(announcement) => { - format!("node {}", announcement.alias()) - } - }, + Some(channel_id) => match channels.iter().find(|c| c.channel_id == *channel_id) + { + None => String::new(), + Some(channel) => { + match nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) { + None => "private node".to_string(), + Some(node) => match &node.announcement_info { + None => "unnamed node".to_string(), + Some(announcement) => { + format!("node {}", announcement.alias()) + } + }, + } } - } - }, - }; - let channel_str = |channel_id: &Option| { - channel_id - .map(|channel_id| format!(" with channel {channel_id}")) - .unwrap_or_default() - }; - let from_prev_str = format!( - " from {}{}", - node_str(&prev_channel_id), - channel_str(&prev_channel_id) - ); - let to_next_str = format!( - " to {}{}", - node_str(&next_channel_id), - channel_str(&next_channel_id) - ); + }, + }; + let channel_str = |channel_id: &Option| { + channel_id + .map(|channel_id| format!(" with channel {channel_id}")) + .unwrap_or_default() + }; - let from_onchain_str = if claim_from_onchain_tx { - "from onchain downstream claim" - } else { - "from HTLC fulfill message" - }; - let amt_args = if let Some(v) = outbound_amount_forwarded_msat { - format!("{v}") - } else { - "?".to_string() - }; - if let Some(fee_earned) = total_fee_earned_msat { - tracing::info!( - "EVENT: Forwarded payment for {} msat{}{}, earning {} msat {}", - amt_args, - from_prev_str, - to_next_str, - fee_earned, - from_onchain_str + let from_prev_str = format!( + " from {}{}", + node_str(&prev_channel_id_copy), + channel_str(&prev_channel_id_copy) ); - } else { - tracing::info!( - "EVENT: Forwarded payment for {} msat{}{}, claiming onchain {}", - amt_args, - from_prev_str, - to_next_str, - from_onchain_str + let to_next_str = format!( + " to {}{}", + node_str(&next_channel_id_copy), + channel_str(&next_channel_id_copy) ); - } + + let from_onchain_str = if claim_from_onchain_tx_copy { + "from onchain downstream claim" + } else { + "from HTLC fulfill message" + }; + let amt_args = if let Some(v) = outbound_amount_forwarded_msat_copy { + format!("{v}") + } else { + "?".to_string() + }; + + if let Some(fee_earned) = total_fee_earned_msat_copy { + tracing::info!( + "EVENT: Forwarded payment for {} msat{}{}, earning {} msat {}", + amt_args, + from_prev_str, + to_next_str, + fee_earned, + from_onchain_str + ); + } else { + tracing::info!( + "EVENT: Forwarded payment for {} msat{}{}, claiming onchain {}", + amt_args, + from_prev_str, + to_next_str, + from_onchain_str + ); + } + }) + .await + .unwrap(); } Event::HTLCHandlingFailed { .. } => {} Event::SpendableOutputs { @@ -1016,7 +1108,12 @@ async fn handle_ldk_events( .join(format!("psbt_{funding_txid}")); if psbt_path.exists() { - let psbt_str = fs::read_to_string(psbt_path).unwrap(); + let psbt_path_copy = psbt_path.clone(); + let psbt_str = tokio::task::spawn_blocking(move || { + fs::read_to_string(psbt_path_copy).unwrap() + }) + .await + .unwrap(); let state_copy = unlocked_state.clone(); let psbt_str_copy = psbt_str.clone(); @@ -1052,7 +1149,15 @@ async fn handle_ldk_events( let consignment = RgbTransfer::load_file(consignment_path).expect("successful consignment load"); - match unlocked_state.rgb_save_new_asset(consignment, funding_txid) { + let unlocked_state_copy = unlocked_state.clone(); + let consignment_copy = consignment; + let funding_txid_copy = funding_txid.clone(); + match tokio::task::spawn_blocking(move || { + unlocked_state_copy.rgb_save_new_asset(consignment_copy, funding_txid_copy) + }) + .await + .unwrap() + { Ok(_) => {} Err(e) if e.to_string().contains("UNIQUE constraint failed") => {} Err(e) => panic!("Failed saving asset: {e}"), @@ -1502,6 +1607,10 @@ pub(crate) async fn start_ldk( mnemonic: Mnemonic, unlock_request: UnlockRequest, ) -> Result<(LdkBackgroundServices, Arc), APIError> { + tracing::info!( + "Starting LDK with network: {:?}", + app_state.static_state.network + ); let static_state = &app_state.static_state; let ldk_data_dir = static_state.ldk_data_dir.clone(); @@ -1512,6 +1621,7 @@ pub(crate) async fn start_ldk( let ldk_peer_listening_port = static_state.ldk_peer_listening_port; // Initialize our bitcoind client. + tracing::info!("Initializing bitcoind client"); let bitcoind_client = match BitcoindClient::new( unlock_request.bitcoind_rpc_host.clone(), unlock_request.bitcoind_rpc_port, @@ -1522,13 +1632,18 @@ pub(crate) async fn start_ldk( ) .await { - Ok(client) => Arc::new(client), + Ok(client) => { + tracing::info!("Bitcoind client initialized successfully"); + Arc::new(client) + } Err(e) => { + tracing::error!("Failed to initialize bitcoind client: {}", e); return Err(APIError::FailedBitcoindConnection(e.to_string())); } }; // Check that the bitcoind we've connected to is running the network we expect + tracing::info!("Checking bitcoind network"); let bitcoind_chain = bitcoind_client.get_blockchain_info().await.chain; if bitcoind_chain != match bitcoin_network { @@ -1541,46 +1656,90 @@ pub(crate) async fn start_ldk( { return Err(APIError::NetworkMismatch(bitcoind_chain, bitcoin_network)); } + tracing::info!("Bitcoind network check passed"); // RGB setup + let storage_dir_path = app_state.static_state.storage_dir_path.clone(); + let indexer_url = if let Some(indexer_url) = &unlock_request.indexer_url { let indexer_protocol = check_indexer_url(indexer_url, bitcoin_network)?; tracing::info!( "Connected to an indexer with the {} protocol", indexer_protocol ); - indexer_url + indexer_url.clone() } else { - tracing::info!("Using the default indexer"); - match bitcoin_network { - BitcoinNetwork::Regtest => ELECTRUM_URL_REGTEST, - BitcoinNetwork::Signet => ELECTRUM_URL_SIGNET, - BitcoinNetwork::Testnet => ELECTRUM_URL_TESTNET, - BitcoinNetwork::Testnet4 => ELECTRUM_URL_TESTNET4, - BitcoinNetwork::Mainnet => ELECTRUM_URL_MAINNET, + tracing::info!("Checking database for saved indexer_url"); + if let Some(saved_url) = app_state + .static_state + .database_manager + .load_rgb_config("indexer_url") + .await? + { + tracing::info!("Using saved indexer_url from database"); + saved_url + } else { + tracing::info!("Using the default indexer"); + match bitcoin_network { + BitcoinNetwork::Regtest => ELECTRUM_URL_REGTEST, + BitcoinNetwork::Signet => ELECTRUM_URL_SIGNET, + BitcoinNetwork::Testnet => ELECTRUM_URL_TESTNET, + BitcoinNetwork::Testnet4 => ELECTRUM_URL_TESTNET4, + BitcoinNetwork::Mainnet => ELECTRUM_URL_MAINNET, + } + .to_string() } }; + let proxy_endpoint = if let Some(proxy_endpoint) = &unlock_request.proxy_endpoint { check_rgb_proxy_endpoint(proxy_endpoint).await?; tracing::info!("Using a custom proxy"); - proxy_endpoint + proxy_endpoint.clone() } else { - tracing::info!("Using the default proxy"); - match bitcoin_network { - BitcoinNetwork::Signet - | BitcoinNetwork::Testnet - | BitcoinNetwork::Testnet4 - | BitcoinNetwork::Mainnet => PROXY_ENDPOINT_PUBLIC, - BitcoinNetwork::Regtest => PROXY_ENDPOINT_LOCAL, + tracing::info!("Checking database for saved proxy_endpoint"); + if let Some(saved_proxy) = app_state + .static_state + .database_manager + .load_rgb_config("proxy_endpoint") + .await? + { + tracing::info!("Using saved proxy_endpoint from database"); + saved_proxy + } else { + tracing::info!("Using the default proxy"); + match bitcoin_network { + BitcoinNetwork::Signet + | BitcoinNetwork::Testnet + | BitcoinNetwork::Testnet4 + | BitcoinNetwork::Mainnet => PROXY_ENDPOINT_PUBLIC, + BitcoinNetwork::Regtest => PROXY_ENDPOINT_LOCAL, + } + .to_string() } }; - let storage_dir_path = app_state.static_state.storage_dir_path.clone(); - fs::write(storage_dir_path.join(INDEXER_URL_FNAME), indexer_url).expect("able to write"); - fs::write( - storage_dir_path.join(BITCOIN_NETWORK_FNAME), - bitcoin_network.to_string(), - ) - .expect("able to write"); + + // Save RGB config to database (source of truth) and sync to files + // Files are needed for rust-lightning library compatibility during wallet operations + app_state + .static_state + .database_manager + .save_rgb_config("indexer_url", &indexer_url) + .await?; + app_state + .static_state + .database_manager + .save_rgb_config("proxy_endpoint", &proxy_endpoint) + .await?; + app_state + .static_state + .database_manager + .save_rgb_config("bitcoin_network", &bitcoin_network.to_string()) + .await?; + app_state + .static_state + .database_manager + .sync_rgb_config_to_files(&storage_dir_path) + .await?; // Initialize the FeeEstimator // BitcoindClient implements the FeeEstimator trait, so it'll act as our fee estimator. @@ -1742,13 +1901,15 @@ pub(crate) async fn start_ldk( get_account_data(bitcoin_network, &mnemonic_str, false).unwrap(); let (_, account_xpub_colored, master_fingerprint) = get_account_data(bitcoin_network, &mnemonic_str, true).unwrap(); + tracing::info!("Creating RGB wallet and going online"); let data_dir = static_state .storage_dir_path .clone() .to_string_lossy() .to_string(); - let mut rgb_wallet = tokio::task::spawn_blocking(move || { - RgbLibWallet::new(WalletData { + let indexer_url_clone = indexer_url.to_string(); + let spawn_result = tokio::task::spawn_blocking(move || { + let mut wallet = RgbLibWallet::new(WalletData { data_dir, bitcoin_network, database_type: DatabaseType::Sqlite, @@ -1760,37 +1921,51 @@ pub(crate) async fn start_ldk( vanilla_keychain: None, supported_schemas: vec![AssetSchema::Nia, AssetSchema::Cfa, AssetSchema::Uda], }) - .expect("valid rgb-lib wallet") + .expect("valid rgb-lib wallet"); + let online = wallet.go_online(false, indexer_url_clone)?; + Ok((wallet, online)) }) .await - .unwrap(); - let rgb_online = rgb_wallet.go_online(false, indexer_url.to_string())?; - fs::write( - static_state.storage_dir_path.join(WALLET_FINGERPRINT_FNAME), - account_xpub_colored.fingerprint().to_string(), - ) - .expect("able to write"); - fs::write( - static_state - .storage_dir_path - .join(WALLET_ACCOUNT_XPUB_COLORED_FNAME), - account_xpub_colored.to_string(), - ) - .expect("able to write"); - fs::write( - static_state - .storage_dir_path - .join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME), - account_xpub_vanilla.to_string(), - ) - .expect("able to write"); - fs::write( - static_state - .storage_dir_path - .join(WALLET_MASTER_FINGERPRINT_FNAME), - master_fingerprint.to_string(), - ) - .expect("able to write"); + .map_err(|e| APIError::Unexpected(format!("Join error: {}", e)))?; + let (rgb_wallet, rgb_online) = spawn_result + .map_err(|e: rgb_lib::Error| APIError::Unexpected(format!("RGB wallet error: {}", e)))?; + tracing::info!("RGB wallet created and online"); + + // Save wallet configs to database (source of truth) and sync to files + app_state + .static_state + .database_manager + .save_rgb_config( + "wallet_fingerprint", + &account_xpub_colored.fingerprint().to_string(), + ) + .await?; + app_state + .static_state + .database_manager + .save_rgb_config( + "wallet_account_xpub_colored", + &account_xpub_colored.to_string(), + ) + .await?; + app_state + .static_state + .database_manager + .save_rgb_config( + "wallet_account_xpub_vanilla", + &account_xpub_vanilla.to_string(), + ) + .await?; + app_state + .static_state + .database_manager + .save_rgb_config("wallet_master_fingerprint", &master_fingerprint.to_string()) + .await?; + app_state + .static_state + .database_manager + .sync_rgb_config_to_files(&static_state.storage_dir_path) + .await?; let rgb_wallet_wrapper = Arc::new(RgbLibWalletWrapper::new( Arc::new(Mutex::new(rgb_wallet)), @@ -2054,10 +2229,15 @@ pub(crate) async fn start_ldk( &ldk_data_dir.join(TAKER_SWAPS_FNAME), ))); - // Read channel IDs info - let channel_ids_map = Arc::new(Mutex::new(disk::read_channel_ids_info( - &ldk_data_dir.join(CHANNEL_IDS_FNAME), - ))); + let database_manager = Arc::clone(&app_state.static_state.database_manager); + database_manager + .migrate_channel_ids_from_file(&ldk_data_dir) + .await?; + + let channel_ids_from_db = database_manager.load_channel_ids().await?; + let channel_ids_map = Arc::new(Mutex::new(ChannelIdsMap { + channel_ids: channel_ids_from_db.into_iter().collect(), + })); let unlocked_state = Arc::new(UnlockedAppState { channel_manager: Arc::clone(&channel_manager), @@ -2078,6 +2258,7 @@ pub(crate) async fn start_ldk( rgb_send_lock: Arc::new(Mutex::new(false)), channel_ids_map, proxy_endpoint: proxy_endpoint.to_string(), + database_manager: Arc::clone(&database_manager), }); let recent_payments_payment_ids = channel_manager @@ -2137,14 +2318,14 @@ pub(crate) async fn start_ldk( // Regularly reconnect to channel peers. let connect_cm = Arc::clone(&channel_manager); let connect_pm = Arc::clone(&peer_manager); - let peer_data_path = ldk_data_dir.join(CHANNEL_PEER_DATA); + let database_manager = Arc::clone(&static_state.database_manager); let stop_connect = Arc::clone(&stop_processing); tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(1)); + let mut interval = tokio::time::interval(Duration::from_secs(3)); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { interval.tick().await; - match disk::read_channel_peer_data(&peer_data_path) { + match database_manager.load_channel_peers().await { Ok(info) => { for node_id in connect_cm .list_channels() @@ -2229,6 +2410,7 @@ pub(crate) async fn start_ldk( tracing::info!("LDK logs are available at /.ldk/logs"); tracing::info!("Local Node ID is {}", channel_manager.get_our_node_id()); + tracing::info!("LDK started successfully"); Ok(( LdkBackgroundServices { diff --git a/src/main.rs b/src/main.rs index aaa98872..1696e6e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ mod args; mod auth; mod backup; mod bitcoind; +mod database; mod disk; +mod entities; mod error; mod ldk; mod rgb; diff --git a/src/routes.rs b/src/routes.rs index 2db37678..f8241005 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -78,7 +78,7 @@ use crate::ldk::{start_ldk, stop_ldk, LdkBackgroundServices, MIN_CHANNEL_CONFIRM use crate::swap::{SwapData, SwapInfo, SwapString}; use crate::utils::{ check_already_initialized, check_channel_id, check_password_strength, check_password_validity, - encrypt_and_save_mnemonic, get_max_local_rgb_amount, get_mnemonic_path, get_route, hex_str, + encrypt_and_save_mnemonic, get_max_local_rgb_amount, get_route, hex_str, hex_str_to_compressed_pubkey, hex_str_to_vec, UnlockedAppState, UserOnionMessageContents, }; use crate::{ @@ -86,7 +86,6 @@ use crate::{ rgb::{check_rgb_proxy_endpoint, get_rgb_channel_info_optional}, }; use crate::{ - disk::{self, CHANNEL_PEER_DATA}, error::APIError, ldk::{PaymentInfo, FEE_RATE, UTXO_SIZE_SAT}, utils::{ @@ -1259,9 +1258,11 @@ pub(crate) async fn address( State(state): State>, ) -> Result, APIError> { let guard = state.check_unlocked().await?; - let unlocked_state = guard.as_ref().unwrap(); + let unlocked_state = Arc::clone(guard.as_ref().unwrap()); - let address = unlocked_state.rgb_get_address()?; + let address = tokio::task::spawn_blocking(move || unlocked_state.rgb_get_address()) + .await + .unwrap()?; Ok(Json(AddressResponse { address })) } @@ -1271,12 +1272,19 @@ pub(crate) async fn asset_balance( WithRejection(Json(payload), _): WithRejection, APIError>, ) -> Result, APIError> { let guard = state.check_unlocked().await?; - let unlocked_state = guard.as_ref().unwrap(); + let unlocked_state = Arc::clone(guard.as_ref().unwrap()); let contract_id = ContractId::from_str(&payload.asset_id) .map_err(|_| APIError::InvalidAssetID(payload.asset_id))?; - let balance = unlocked_state.rgb_get_asset_balance(contract_id)?; + let unlocked_state_clone = Arc::clone(&unlocked_state); + let contract_id_clone = contract_id; + let balance = tokio::task::spawn_blocking(move || { + unlocked_state_clone.rgb_get_asset_balance(contract_id_clone) + }) + .await + .unwrap() + .map_err(|e| APIError::FailedBroadcast(format!("Asset balance retrieval failed: {e}")))?; let mut offchain_outbound = 0; let mut offchain_inbound = 0; @@ -1341,7 +1349,8 @@ pub(crate) async fn backup( let _guard = state.check_locked().await?; let _mnemonic = - check_password_validity(&payload.password, &state.static_state.storage_dir_path)?; + check_password_validity(&payload.password, &state.static_state.database_manager) + .await?; do_backup( &state.static_state.storage_dir_path, @@ -1388,13 +1397,15 @@ pub(crate) async fn change_password( check_password_strength(payload.new_password.clone())?; let mnemonic = - check_password_validity(&payload.old_password, &state.static_state.storage_dir_path)?; + check_password_validity(&payload.old_password, &state.static_state.database_manager) + .await?; encrypt_and_save_mnemonic( payload.new_password, mnemonic.to_string(), - &get_mnemonic_path(&state.static_state.storage_dir_path), - )?; + &state.static_state.database_manager, + ) + .await?; Ok(Json(EmptyResponse {})) }) @@ -1509,11 +1520,11 @@ pub(crate) async fn connect_peer( if let Some(peer_addr) = peer_addr { connect_peer_if_necessary(peer_pubkey, peer_addr, unlocked_state.peer_manager.clone()) .await?; - disk::persist_channel_peer( - &state.static_state.ldk_data_dir.join(CHANNEL_PEER_DATA), - &peer_pubkey, - &peer_addr, - )?; + state + .static_state + .database_manager + .save_channel_peer(&peer_pubkey, &peer_addr) + .await?; } else { return Err(APIError::InvalidPeerInfo(s!( "incorrectly formatted peer info. Should be formatted as: `pubkey@host:port`" @@ -1531,15 +1542,19 @@ pub(crate) async fn create_utxos( ) -> Result, APIError> { no_cancel(async move { let guard = state.check_unlocked().await?; - let unlocked_state = guard.as_ref().unwrap(); - - unlocked_state.rgb_create_utxos( - payload.up_to, - payload.num.unwrap_or(UTXO_NUM), - payload.size.unwrap_or(UTXO_SIZE_SAT), - payload.fee_rate, - payload.skip_sync, - )?; + let unlocked_state = Arc::clone(guard.as_ref().unwrap()); + tokio::task::spawn_blocking(move || { + unlocked_state.rgb_create_utxos( + payload.up_to, + payload.num.unwrap_or(UTXO_NUM), + payload.size.unwrap_or(UTXO_SIZE_SAT), + payload.fee_rate, + payload.skip_sync, + ) + }) + .await + .unwrap() + .map_err(|e| APIError::FailedBroadcast(format!("UTXO creation failed: {e}")))?; tracing::debug!("UTXO creation complete"); Ok(Json(EmptyResponse {})) @@ -1614,10 +1629,11 @@ pub(crate) async fn disconnect_peer( } } - disk::delete_channel_peer( - &state.static_state.ldk_data_dir.join(CHANNEL_PEER_DATA), - payload.peer_pubkey, - )?; + state + .static_state + .database_manager + .delete_channel_peer(&peer_pubkey) + .await?; //check the pubkey matches a valid connected peer if unlocked_state @@ -1724,14 +1740,18 @@ pub(crate) async fn init( check_password_strength(payload.password.clone())?; - let mnemonic_path = get_mnemonic_path(&state.static_state.storage_dir_path); - check_already_initialized(&mnemonic_path)?; + check_already_initialized(&state.static_state.database_manager).await?; let keys = generate_keys(state.static_state.network); let mnemonic = keys.mnemonic; - encrypt_and_save_mnemonic(payload.password, mnemonic.clone(), &mnemonic_path)?; + encrypt_and_save_mnemonic( + payload.password, + mnemonic.clone(), + &state.static_state.database_manager, + ) + .await?; Ok(Json(InitResponse { mnemonic })) }) @@ -1805,18 +1825,23 @@ pub(crate) async fn issue_asset_nia( ) -> Result, APIError> { no_cancel(async move { let guard = state.check_unlocked().await?; - let unlocked_state = guard.as_ref().unwrap(); + let unlocked_state = Arc::clone(guard.as_ref().unwrap()); if *unlocked_state.rgb_send_lock.lock().unwrap() { return Err(APIError::OpenChannelInProgress); } - let asset = unlocked_state.rgb_issue_asset_nia( - payload.ticker, - payload.name, - payload.precision, - payload.amounts, - )?; + let asset = tokio::task::spawn_blocking(move || { + unlocked_state.rgb_issue_asset_nia( + payload.ticker, + payload.name, + payload.precision, + payload.amounts, + ) + }) + .await + .unwrap() + .map_err(|e| APIError::FailedBroadcast(format!("Asset issuance failed: {e}")))?; Ok(Json(IssueAssetNIAResponse { asset: asset.into(), @@ -2049,76 +2074,83 @@ pub(crate) async fn list_channels( let guard = state.check_unlocked().await?; let unlocked_state = guard.as_ref().unwrap(); - let mut channels = vec![]; - for chan_info in unlocked_state.channel_manager.list_channels() { - let status = match chan_info.channel_shutdown_state.unwrap() { - ChannelShutdownState::NotShuttingDown => { - if chan_info.is_channel_ready { - ChannelStatus::Opened - } else { - ChannelStatus::Opening + let unlocked_state_copy = Arc::clone(unlocked_state); + let ldk_data_dir = state.static_state.ldk_data_dir.clone(); + let channels = tokio::task::spawn_blocking(move || { + let mut channels = vec![]; + for chan_info in unlocked_state_copy.channel_manager.list_channels() { + let status = match chan_info.channel_shutdown_state.unwrap() { + ChannelShutdownState::NotShuttingDown => { + if chan_info.is_channel_ready { + ChannelStatus::Opened + } else { + ChannelStatus::Opening + } + } + _ => ChannelStatus::Closing, + }; + let mut channel = Channel { + channel_id: chan_info.channel_id.0.as_hex().to_string(), + peer_pubkey: hex_str(&chan_info.counterparty.node_id.serialize()), + status, + ready: chan_info.is_channel_ready, + capacity_sat: chan_info.channel_value_satoshis, + outbound_balance_msat: chan_info.outbound_capacity_msat, + inbound_balance_msat: chan_info.inbound_capacity_msat, + next_outbound_htlc_limit_msat: chan_info.next_outbound_htlc_limit_msat, + next_outbound_htlc_minimum_msat: chan_info.next_outbound_htlc_minimum_msat, + is_usable: chan_info.is_usable, + public: chan_info.is_announced, + ..Default::default() + }; + + if let Some(funding_txo) = chan_info.funding_txo { + channel.funding_txid = Some(funding_txo.txid.to_string()); + if let Ok(chan_monitor) = unlocked_state_copy + .chain_monitor + .get_monitor(chan_info.channel_id) + { + channel.local_balance_sat = chan_monitor + .get_claimable_balances() + .iter() + .map(|b| b.claimable_amount_satoshis()) + .sum::(); } } - _ => ChannelStatus::Closing, - }; - let mut channel = Channel { - channel_id: chan_info.channel_id.0.as_hex().to_string(), - peer_pubkey: hex_str(&chan_info.counterparty.node_id.serialize()), - status, - ready: chan_info.is_channel_ready, - capacity_sat: chan_info.channel_value_satoshis, - outbound_balance_msat: chan_info.outbound_capacity_msat, - inbound_balance_msat: chan_info.inbound_capacity_msat, - next_outbound_htlc_limit_msat: chan_info.next_outbound_htlc_limit_msat, - next_outbound_htlc_minimum_msat: chan_info.next_outbound_htlc_minimum_msat, - is_usable: chan_info.is_usable, - public: chan_info.is_announced, - ..Default::default() - }; - if let Some(funding_txo) = chan_info.funding_txo { - channel.funding_txid = Some(funding_txo.txid.to_string()); - if let Ok(chan_monitor) = unlocked_state - .chain_monitor - .get_monitor(chan_info.channel_id) + if let Some(node_info) = unlocked_state_copy + .network_graph + .read_only() + .nodes() + .get(&NodeId::from_pubkey(&chan_info.counterparty.node_id)) { - channel.local_balance_sat = chan_monitor - .get_claimable_balances() - .iter() - .map(|b| b.claimable_amount_satoshis()) - .sum::(); + if let Some(announcement) = &node_info.announcement_info { + channel.peer_alias = Some(announcement.alias().to_string()); + } } - } - if let Some(node_info) = unlocked_state - .network_graph - .read_only() - .nodes() - .get(&NodeId::from_pubkey(&chan_info.counterparty.node_id)) - { - if let Some(announcement) = &node_info.announcement_info { - channel.peer_alias = Some(announcement.alias().to_string()); + if let Some(id) = chan_info.short_channel_id { + channel.short_channel_id = Some(id); } - } - if let Some(id) = chan_info.short_channel_id { - channel.short_channel_id = Some(id); - } - - let info_file_path = get_rgb_channel_info_path( - &chan_info.channel_id.0.as_hex().to_string(), - &state.static_state.ldk_data_dir, - false, - ); - if info_file_path.exists() { - let rgb_info = parse_rgb_channel_info(&info_file_path); - channel.asset_id = Some(rgb_info.contract_id.to_string()); - channel.asset_local_amount = Some(rgb_info.local_rgb_amount); - channel.asset_remote_amount = Some(rgb_info.remote_rgb_amount); - }; + let info_file_path = get_rgb_channel_info_path( + &chan_info.channel_id.0.as_hex().to_string(), + &ldk_data_dir, + false, + ); + if info_file_path.exists() { + let rgb_info = parse_rgb_channel_info(&info_file_path); + channel.asset_id = Some(rgb_info.contract_id.to_string()); + channel.asset_local_amount = Some(rgb_info.local_rgb_amount); + channel.asset_remote_amount = Some(rgb_info.remote_rgb_amount); + }; - channels.push(channel); - } + channels.push(channel); + } + channels + }) + .await + .unwrap(); Ok(Json(ListChannelsResponse { channels })) } @@ -3027,7 +3059,6 @@ pub(crate) async fn open_channel( let (peer_pubkey, mut peer_addr) = parse_peer_info(payload.peer_pubkey_and_opt_addr.to_string())?; - let peer_data_path = state.static_state.ldk_data_dir.join(CHANNEL_PEER_DATA); if peer_addr.is_none() { if let Some(peer) = unlocked_state.peer_manager.peer_by_node_id(&peer_pubkey) { if let Some(socket_address) = peer.socket_address { @@ -3039,7 +3070,11 @@ pub(crate) async fn open_channel( } } if peer_addr.is_none() { - let peer_info = disk::read_channel_peer_data(&peer_data_path)?; + let peer_info = state + .static_state + .database_manager + .load_channel_peers() + .await?; for (pubkey, addr) in peer_info.into_iter() { if pubkey == peer_pubkey { peer_addr = Some(addr); @@ -3050,7 +3085,11 @@ pub(crate) async fn open_channel( if let Some(peer_addr) = peer_addr { connect_peer_if_necessary(peer_pubkey, peer_addr, unlocked_state.peer_manager.clone()) .await?; - disk::persist_channel_peer(&peer_data_path, &peer_pubkey, &peer_addr)?; + state + .static_state + .database_manager + .save_channel_peer(&peer_pubkey, &peer_addr) + .await?; } else { return Err(APIError::InvalidPeerInfo(s!( "cannot find the address for the provided pubkey" @@ -3082,7 +3121,13 @@ pub(crate) async fn open_channel( }; let consignment_endpoint = if let Some((contract_id, asset_amount)) = &colored_info { - let balance = unlocked_state.rgb_get_asset_balance(*contract_id)?; + let unlocked_state_clone = Arc::clone(unlocked_state); + let contract_id_clone = *contract_id; + let balance = tokio::task::spawn_blocking(move || { + unlocked_state_clone.rgb_get_asset_balance(contract_id_clone) + }) + .await + .unwrap()?; let spendable_rgb_amount = balance.spendable; if *asset_amount > spendable_rgb_amount { @@ -3100,9 +3145,14 @@ pub(crate) async fn open_channel( let script_buf = ScriptBuf::from_bytes(fake_p2wsh.to_vec()); let recipient_id = recipient_id_from_script_buf(script_buf, state.static_state.network); let asset_id = contract_id.to_string(); - let schema = unlocked_state - .rgb_get_asset_metadata(*contract_id)? - .asset_schema; + let unlocked_state_clone = Arc::clone(unlocked_state); + let contract_id_clone = *contract_id; + let schema = tokio::task::spawn_blocking(move || { + unlocked_state_clone.rgb_get_asset_metadata(contract_id_clone) + }) + .await + .unwrap()? + .asset_schema; let assignment = match schema { RgbLibAssetSchema::Nia | RgbLibAssetSchema::Cfa => { Assignment::Fungible(*asset_amount) @@ -3283,8 +3333,7 @@ pub(crate) async fn restore( no_cancel(async move { let _unlocked_state = state.check_locked().await?; - let mnemonic_path = get_mnemonic_path(&state.static_state.storage_dir_path); - check_already_initialized(&mnemonic_path)?; + check_already_initialized(&state.static_state.database_manager).await?; restore_backup( Path::new(&payload.backup_path), @@ -3293,7 +3342,8 @@ pub(crate) async fn restore( )?; let _mnemonic = - check_password_validity(&payload.password, &state.static_state.storage_dir_path)?; + check_password_validity(&payload.password, &state.static_state.database_manager) + .await?; Ok(Json(EmptyResponse {})) }) @@ -3310,7 +3360,7 @@ pub(crate) async fn revoke_token( let token_to_revoke = Biscuit::from_base64(&payload.token, root_pubkey) .map_err(|_| APIError::InvalidBiscuitToken)?; - state.revoke_token(&token_to_revoke)?; + state.revoke_token(&token_to_revoke).await?; Ok(Json(EmptyResponse {})) } @@ -3752,16 +3802,16 @@ pub(crate) async fn unlock( } } - let mnemonic = match check_password_validity( - &payload.password, - &state.static_state.storage_dir_path, - ) { - Ok(mnemonic) => mnemonic, - Err(e) => { - state.update_changing_state(false); - return Err(e); - } - }; + let mnemonic = + match check_password_validity(&payload.password, &state.static_state.database_manager) + .await + { + Ok(mnemonic) => mnemonic, + Err(e) => { + state.update_changing_state(false); + return Err(e); + } + }; tracing::debug!("Starting LDK..."); let (new_ldk_background_services, new_unlocked_app_state) = diff --git a/src/test/authentication.rs b/src/test/authentication.rs index e4f7a735..0d721a9a 100644 --- a/src/test/authentication.rs +++ b/src/test/authentication.rs @@ -23,7 +23,7 @@ fn create_token( } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn authentication() { initialize(); diff --git a/src/test/backup_and_restore.rs b/src/test/backup_and_restore.rs index 1a358bb3..bb3aef11 100644 --- a/src/test/backup_and_restore.rs +++ b/src/test/backup_and_restore.rs @@ -4,7 +4,7 @@ use regex::RegexSet; const TEST_DIR_BASE: &str = "tmp/backup_and_restore/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn backup_and_restore() { initialize(); diff --git a/src/test/close_coop_nobtc_acceptor.rs b/src/test/close_coop_nobtc_acceptor.rs index 69baf603..1994807a 100644 --- a/src/test/close_coop_nobtc_acceptor.rs +++ b/src/test/close_coop_nobtc_acceptor.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_coop_nobtc_acceptor/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_coop_nobtc_acceptor() { initialize(); diff --git a/src/test/close_coop_other_side.rs b/src/test/close_coop_other_side.rs index 80c71c44..6a303ad8 100644 --- a/src/test/close_coop_other_side.rs +++ b/src/test/close_coop_other_side.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_coop_other_side/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_coop_other_side() { initialize(); diff --git a/src/test/close_coop_standard.rs b/src/test/close_coop_standard.rs index 54ddd02c..17c8928b 100644 --- a/src/test/close_coop_standard.rs +++ b/src/test/close_coop_standard.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_coop_standard/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_coop_standard() { initialize(); diff --git a/src/test/close_coop_vanilla.rs b/src/test/close_coop_vanilla.rs index 055b2601..0f0bfd38 100644 --- a/src/test/close_coop_vanilla.rs +++ b/src/test/close_coop_vanilla.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_coop_vanilla/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn with_anchors() { initialize(); @@ -78,7 +78,7 @@ async fn with_anchors() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn without_anchors() { initialize(); diff --git a/src/test/close_coop_zero_balance.rs b/src/test/close_coop_zero_balance.rs index 3d3fcf87..a74ff73b 100644 --- a/src/test/close_coop_zero_balance.rs +++ b/src/test/close_coop_zero_balance.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_coop_zero_balance/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_coop_zero_balance() { initialize(); diff --git a/src/test/close_force_nobtc_acceptor.rs b/src/test/close_force_nobtc_acceptor.rs index 4f5038ae..043aefa9 100644 --- a/src/test/close_force_nobtc_acceptor.rs +++ b/src/test/close_force_nobtc_acceptor.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_force_nobtc_acceptor/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_force_nobtc_acceptor() { initialize(); diff --git a/src/test/close_force_other_side.rs b/src/test/close_force_other_side.rs index d0845c49..43def871 100644 --- a/src/test/close_force_other_side.rs +++ b/src/test/close_force_other_side.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_force_other_side/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_force_other_side() { initialize(); diff --git a/src/test/close_force_standard.rs b/src/test/close_force_standard.rs index 8535c379..915033ad 100644 --- a/src/test/close_force_standard.rs +++ b/src/test/close_force_standard.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/close_force_standard/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn close_force_standard() { initialize(); diff --git a/src/test/concurrent_btc_payments.rs b/src/test/concurrent_btc_payments.rs index b039fe18..c1ca3c80 100644 --- a/src/test/concurrent_btc_payments.rs +++ b/src/test/concurrent_btc_payments.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/concurrent_btc_payments/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn concurrent_btc_payments() { initialize(); diff --git a/src/test/concurrent_openchannel.rs b/src/test/concurrent_openchannel.rs index 3db73a8a..0fb8322f 100644 --- a/src/test/concurrent_openchannel.rs +++ b/src/test/concurrent_openchannel.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/concurrent_openchannel/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn concurrent_openchannel() { initialize(); diff --git a/src/test/fail_transfers.rs b/src/test/fail_transfers.rs index 8be7c4f2..abbda253 100644 --- a/src/test/fail_transfers.rs +++ b/src/test/fail_transfers.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/fail_transfers/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn success() { initialize(); diff --git a/src/test/getchannelid.rs b/src/test/getchannelid.rs index 18ed3476..2ca6497f 100644 --- a/src/test/getchannelid.rs +++ b/src/test/getchannelid.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/getchannelid/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn getchannelid_success() { initialize(); @@ -55,7 +55,7 @@ async fn getchannelid_success() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn getchannelid_fail() { initialize(); diff --git a/src/test/htlc_amount_checks.rs b/src/test/htlc_amount_checks.rs index 1b9f9c1e..4470a6ae 100644 --- a/src/test/htlc_amount_checks.rs +++ b/src/test/htlc_amount_checks.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/htlc_amount_checks/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn htlc_amount_checks_3nodes() { initialize(); @@ -83,7 +83,7 @@ async fn htlc_amount_checks_3nodes() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn htlc_amount_checks_2nodes() { initialize(); diff --git a/src/test/invoice.rs b/src/test/invoice.rs index c65a517a..c3a8037c 100644 --- a/src/test/invoice.rs +++ b/src/test/invoice.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/invoice/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn invoice() { initialize(); diff --git a/src/test/issue.rs b/src/test/issue.rs index 8a9592c2..f06c8f4e 100644 --- a/src/test/issue.rs +++ b/src/test/issue.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/issue/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn issue() { initialize(); diff --git a/src/test/lock_unlock_changepassword.rs b/src/test/lock_unlock_changepassword.rs index 04cd08a5..37f757ca 100644 --- a/src/test/lock_unlock_changepassword.rs +++ b/src/test/lock_unlock_changepassword.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/lock_unlock_changepassword/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn lock_unlock_changepassword() { initialize(); diff --git a/src/test/mod.rs b/src/test/mod.rs index 06b9a120..8e9a7b49 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1811,6 +1811,7 @@ mod invoice; mod issue; mod lock_unlock_changepassword; mod multi_hop; +mod rgb_config_persistence; mod multi_open_close; mod open_after_double_send; mod openchannel_fail; diff --git a/src/test/multi_hop.rs b/src/test/multi_hop.rs index 99299db2..6540507e 100644 --- a/src/test/multi_hop.rs +++ b/src/test/multi_hop.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/multi_hop/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn multi_hop() { initialize(); diff --git a/src/test/multi_open_close.rs b/src/test/multi_open_close.rs index d65f4ad8..a924dca2 100644 --- a/src/test/multi_open_close.rs +++ b/src/test/multi_open_close.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/multi_open_close/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn multi_open_close() { initialize(); diff --git a/src/test/open_after_double_send.rs b/src/test/open_after_double_send.rs index 46e7b3c9..3920ca98 100644 --- a/src/test/open_after_double_send.rs +++ b/src/test/open_after_double_send.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/open_after_double_send/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn open_after_double_send() { initialize(); diff --git a/src/test/openchannel_fail.rs b/src/test/openchannel_fail.rs index 32dee214..362dc761 100644 --- a/src/test/openchannel_fail.rs +++ b/src/test/openchannel_fail.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/open_fail/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn open_fail() { initialize(); diff --git a/src/test/openchannel_optional_addr.rs b/src/test/openchannel_optional_addr.rs index a86555df..811e5108 100644 --- a/src/test/openchannel_optional_addr.rs +++ b/src/test/openchannel_optional_addr.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/openchannel_optional_addr/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn openchannel_optional_addr_forward() { initialize(); @@ -84,7 +84,7 @@ async fn openchannel_optional_addr_forward() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn openchannel_optional_addr_reverse() { initialize(); diff --git a/src/test/payment.rs b/src/test/payment.rs index b3ae30af..767018c9 100644 --- a/src/test/payment.rs +++ b/src/test/payment.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/payment/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn success() { initialize(); @@ -210,7 +210,7 @@ async fn success() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn same_invoice_twice() { initialize(); diff --git a/src/test/refuse_high_fees.rs b/src/test/refuse_high_fees.rs index 8d84614d..03286dce 100644 --- a/src/test/refuse_high_fees.rs +++ b/src/test/refuse_high_fees.rs @@ -11,7 +11,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/refuse_high_fees/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn refuse_high_fees() { initialize(); diff --git a/src/test/restart.rs b/src/test/restart.rs index 0094743b..160f1d3e 100644 --- a/src/test/restart.rs +++ b/src/test/restart.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/restart/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn restart() { initialize(); diff --git a/src/test/rgb_config_persistence.rs b/src/test/rgb_config_persistence.rs new file mode 100644 index 00000000..e9e63a31 --- /dev/null +++ b/src/test/rgb_config_persistence.rs @@ -0,0 +1,397 @@ +use std::fs; +use tempfile::TempDir; +use crate::database::DatabaseManager; + +const INDEXER_URL_FNAME: &str = "indexer_url"; +const PROXY_ENDPOINT_FNAME: &str = "proxy_endpoint"; +const BITCOIN_NETWORK_FNAME: &str = "bitcoin_network"; +const WALLET_FINGERPRINT_FNAME: &str = "wallet_fingerprint"; +const WALLET_ACCOUNT_XPUB_COLORED_FNAME: &str = "wallet_account_xpub_colored"; +const WALLET_ACCOUNT_XPUB_VANILLA_FNAME: &str = "wallet_account_xpub_vanilla"; +const WALLET_MASTER_FINGERPRINT_FNAME: &str = "wallet_master_fingerprint"; + +#[tokio::test] +async fn test_save_and_load_rgb_config() { + // Create a temporary database + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Test saving and loading indexer_url + let indexer_url = "127.0.0.1:50001"; + db_manager.save_rgb_config("indexer_url", indexer_url).await.unwrap(); + let loaded = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded, Some(indexer_url.to_string())); + + // Test saving and loading proxy_endpoint + let proxy_endpoint = "rpc://127.0.0.1:3000/json-rpc"; + db_manager.save_rgb_config("proxy_endpoint", proxy_endpoint).await.unwrap(); + let loaded = db_manager.load_rgb_config("proxy_endpoint").await.unwrap(); + assert_eq!(loaded, Some(proxy_endpoint.to_string())); + + // Test saving and loading bitcoin_network + let bitcoin_network = "regtest"; + db_manager.save_rgb_config("bitcoin_network", bitcoin_network).await.unwrap(); + let loaded = db_manager.load_rgb_config("bitcoin_network").await.unwrap(); + assert_eq!(loaded, Some(bitcoin_network.to_string())); + + // Test saving and loading wallet_fingerprint + let wallet_fingerprint = "fingerprint123"; + db_manager.save_rgb_config("wallet_fingerprint", wallet_fingerprint).await.unwrap(); + let loaded = db_manager.load_rgb_config("wallet_fingerprint").await.unwrap(); + assert_eq!(loaded, Some(wallet_fingerprint.to_string())); + + // Test saving and loading wallet_account_xpub_colored + let wallet_account_xpub_colored = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + db_manager.save_rgb_config("wallet_account_xpub_colored", wallet_account_xpub_colored).await.unwrap(); + let loaded = db_manager.load_rgb_config("wallet_account_xpub_colored").await.unwrap(); + assert_eq!(loaded, Some(wallet_account_xpub_colored.to_string())); + + // Test saving and loading wallet_account_xpub_vanilla + let wallet_account_xpub_vanilla = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + db_manager.save_rgb_config("wallet_account_xpub_vanilla", wallet_account_xpub_vanilla).await.unwrap(); + let loaded = db_manager.load_rgb_config("wallet_account_xpub_vanilla").await.unwrap(); + assert_eq!(loaded, Some(wallet_account_xpub_vanilla.to_string())); + + // Test saving and loading wallet_master_fingerprint + let wallet_master_fingerprint = "master_fingerprint_123"; + db_manager.save_rgb_config("wallet_master_fingerprint", wallet_master_fingerprint).await.unwrap(); + let loaded = db_manager.load_rgb_config("wallet_master_fingerprint").await.unwrap(); + assert_eq!(loaded, Some(wallet_master_fingerprint.to_string())); + + // Test loading non-existent key + let loaded = db_manager.load_rgb_config("non_existent").await.unwrap(); + assert_eq!(loaded, None); + + // Test updating existing key + let new_indexer_url = "127.0.0.1:50002"; + db_manager.save_rgb_config("indexer_url", new_indexer_url).await.unwrap(); + let loaded = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded, Some(new_indexer_url.to_string())); +} + +#[tokio::test] +async fn test_rgb_config_cache() { + // Create a temporary database + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Save a value + let indexer_url = "127.0.0.1:50001"; + db_manager.save_rgb_config("indexer_url", indexer_url).await.unwrap(); + + // First load should hit DB and cache + let loaded1 = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded1, Some(indexer_url.to_string())); + + // Second load should hit cache + let loaded2 = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded2, Some(indexer_url.to_string())); + + // Verify cache is updated on save + let new_indexer_url = "127.0.0.1:50002"; + db_manager.save_rgb_config("indexer_url", new_indexer_url).await.unwrap(); + let loaded3 = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded3, Some(new_indexer_url.to_string())); +} + +#[tokio::test] +async fn test_sync_rgb_config_to_files() { + // Create a temporary database and directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Save config values + let indexer_url = "127.0.0.1:50001"; + let proxy_endpoint = "rpc://127.0.0.1:3000/json-rpc"; + let bitcoin_network = "regtest"; + let wallet_fingerprint = "fingerprint123"; + let wallet_account_xpub_colored = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + let wallet_account_xpub_vanilla = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + let wallet_master_fingerprint = "master_fingerprint_123"; + + db_manager.save_rgb_config("indexer_url", indexer_url).await.unwrap(); + db_manager.save_rgb_config("proxy_endpoint", proxy_endpoint).await.unwrap(); + db_manager.save_rgb_config("bitcoin_network", bitcoin_network).await.unwrap(); + db_manager.save_rgb_config("wallet_fingerprint", wallet_fingerprint).await.unwrap(); + db_manager.save_rgb_config("wallet_account_xpub_colored", wallet_account_xpub_colored).await.unwrap(); + db_manager.save_rgb_config("wallet_account_xpub_vanilla", wallet_account_xpub_vanilla).await.unwrap(); + db_manager.save_rgb_config("wallet_master_fingerprint", wallet_master_fingerprint).await.unwrap(); + + // Sync to files + db_manager.sync_rgb_config_to_files(temp_dir.path()).await.unwrap(); + + // Verify files exist and contain correct content + let indexer_file = temp_dir.path().join(INDEXER_URL_FNAME); + let proxy_file = temp_dir.path().join(PROXY_ENDPOINT_FNAME); + let bitcoin_network_file = temp_dir.path().join(BITCOIN_NETWORK_FNAME); + let wallet_fingerprint_file = temp_dir.path().join(WALLET_FINGERPRINT_FNAME); + let wallet_account_xpub_colored_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_COLORED_FNAME); + let wallet_account_xpub_vanilla_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME); + let wallet_master_fingerprint_file = temp_dir.path().join(WALLET_MASTER_FINGERPRINT_FNAME); + + assert!(indexer_file.exists()); + assert!(proxy_file.exists()); + assert!(bitcoin_network_file.exists()); + assert!(wallet_fingerprint_file.exists()); + assert!(wallet_account_xpub_colored_file.exists()); + assert!(wallet_account_xpub_vanilla_file.exists()); + assert!(wallet_master_fingerprint_file.exists()); + + let indexer_content = fs::read_to_string(&indexer_file).unwrap(); + let proxy_content = fs::read_to_string(&proxy_file).unwrap(); + let bitcoin_network_content = fs::read_to_string(&bitcoin_network_file).unwrap(); + let wallet_fingerprint_content = fs::read_to_string(&wallet_fingerprint_file).unwrap(); + let wallet_account_xpub_colored_content = fs::read_to_string(&wallet_account_xpub_colored_file).unwrap(); + let wallet_account_xpub_vanilla_content = fs::read_to_string(&wallet_account_xpub_vanilla_file).unwrap(); + let wallet_master_fingerprint_content = fs::read_to_string(&wallet_master_fingerprint_file).unwrap(); + + assert_eq!(indexer_content.trim(), indexer_url); + assert_eq!(proxy_content.trim(), proxy_endpoint); + assert_eq!(bitcoin_network_content.trim(), bitcoin_network); + assert_eq!(wallet_fingerprint_content.trim(), wallet_fingerprint); + assert_eq!(wallet_account_xpub_colored_content.trim(), wallet_account_xpub_colored); + assert_eq!(wallet_account_xpub_vanilla_content.trim(), wallet_account_xpub_vanilla); + assert_eq!(wallet_master_fingerprint_content.trim(), wallet_master_fingerprint); +} + +#[tokio::test] +async fn test_migrate_indexer_url_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with indexer_url + let indexer_url = "127.0.0.1:50001"; + let indexer_file = temp_dir.path().join(INDEXER_URL_FNAME); + fs::write(&indexer_file, indexer_url).unwrap(); + + // Migrate from file to DB + db_manager.migrate_indexer_url_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded, Some(indexer_url.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(indexer_file.exists()); + let file_content = fs::read_to_string(&indexer_file).unwrap(); + assert_eq!(file_content.trim(), indexer_url); +} + +#[tokio::test] +async fn test_migrate_no_file_present() { + // Create a temporary directory without indexer_url file + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Try to migrate when no file exists + db_manager.migrate_indexer_url_from_file(temp_dir.path()).await.unwrap(); + + // Verify no value is set in DB + let loaded = db_manager.load_rgb_config("indexer_url").await.unwrap(); + assert_eq!(loaded, None); +} + +#[tokio::test] +async fn test_sync_empty_config_to_files() { + // Create a temporary database and directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // No config saved, sync to files + db_manager.sync_rgb_config_to_files(temp_dir.path()).await.unwrap(); + + // Verify no files are created + let indexer_file = temp_dir.path().join(INDEXER_URL_FNAME); + let proxy_file = temp_dir.path().join(PROXY_ENDPOINT_FNAME); + let bitcoin_network_file = temp_dir.path().join(BITCOIN_NETWORK_FNAME); + let wallet_fingerprint_file = temp_dir.path().join(WALLET_FINGERPRINT_FNAME); + let wallet_account_xpub_colored_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_COLORED_FNAME); + let wallet_account_xpub_vanilla_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME); + let wallet_master_fingerprint_file = temp_dir.path().join(WALLET_MASTER_FINGERPRINT_FNAME); + + assert!(!indexer_file.exists()); + assert!(!proxy_file.exists()); + assert!(!bitcoin_network_file.exists()); + assert!(!wallet_fingerprint_file.exists()); + assert!(!wallet_account_xpub_colored_file.exists()); + assert!(!wallet_account_xpub_vanilla_file.exists()); + assert!(!wallet_master_fingerprint_file.exists()); +} + +#[tokio::test] +async fn test_sync_partial_config_to_files() { + // Create a temporary database and directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Save only indexer_url + let indexer_url = "127.0.0.1:50001"; + db_manager.save_rgb_config("indexer_url", indexer_url).await.unwrap(); + + // Sync to files + db_manager.sync_rgb_config_to_files(temp_dir.path()).await.unwrap(); + + // Verify only indexer_url file exists + let indexer_file = temp_dir.path().join(INDEXER_URL_FNAME); + let proxy_file = temp_dir.path().join(PROXY_ENDPOINT_FNAME); + + assert!(indexer_file.exists()); + assert!(!proxy_file.exists()); + + let indexer_content = fs::read_to_string(&indexer_file).unwrap(); + assert_eq!(indexer_content.trim(), indexer_url); +} + +#[tokio::test] +async fn test_overwrite_file_on_sync() { + // Create a temporary database and directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create file with old content + let indexer_file = temp_dir.path().join(INDEXER_URL_FNAME); + fs::write(&indexer_file, "old.url:9999").unwrap(); + + // Save new config and sync + let new_indexer_url = "127.0.0.1:50001"; + db_manager.save_rgb_config("indexer_url", new_indexer_url).await.unwrap(); + db_manager.sync_rgb_config_to_files(temp_dir.path()).await.unwrap(); + + // Verify file content is overwritten + let indexer_content = fs::read_to_string(&indexer_file).unwrap(); + assert_eq!(indexer_content.trim(), new_indexer_url); +} + +#[tokio::test] +async fn test_migrate_bitcoin_network_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with bitcoin_network + let bitcoin_network = "regtest"; + let bitcoin_network_file = temp_dir.path().join(BITCOIN_NETWORK_FNAME); + fs::write(&bitcoin_network_file, bitcoin_network).unwrap(); + + // Migrate from file to DB + db_manager.migrate_bitcoin_network_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("bitcoin_network").await.unwrap(); + assert_eq!(loaded, Some(bitcoin_network.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(bitcoin_network_file.exists()); + let file_content = fs::read_to_string(&bitcoin_network_file).unwrap(); + assert_eq!(file_content.trim(), bitcoin_network); +} + +#[tokio::test] +async fn test_migrate_wallet_fingerprint_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with wallet_fingerprint + let wallet_fingerprint = "fingerprint123"; + let wallet_fingerprint_file = temp_dir.path().join(WALLET_FINGERPRINT_FNAME); + fs::write(&wallet_fingerprint_file, wallet_fingerprint).unwrap(); + + // Migrate from file to DB + db_manager.migrate_wallet_fingerprint_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("wallet_fingerprint").await.unwrap(); + assert_eq!(loaded, Some(wallet_fingerprint.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(wallet_fingerprint_file.exists()); + let file_content = fs::read_to_string(&wallet_fingerprint_file).unwrap(); + assert_eq!(file_content.trim(), wallet_fingerprint); +} + +#[tokio::test] +async fn test_migrate_wallet_account_xpub_colored_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with wallet_account_xpub_colored + let wallet_account_xpub_colored = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + let wallet_account_xpub_colored_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_COLORED_FNAME); + fs::write(&wallet_account_xpub_colored_file, wallet_account_xpub_colored).unwrap(); + + // Migrate from file to DB + db_manager.migrate_wallet_account_xpub_colored_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("wallet_account_xpub_colored").await.unwrap(); + assert_eq!(loaded, Some(wallet_account_xpub_colored.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(wallet_account_xpub_colored_file.exists()); + let file_content = fs::read_to_string(&wallet_account_xpub_colored_file).unwrap(); + assert_eq!(file_content.trim(), wallet_account_xpub_colored); +} + +#[tokio::test] +async fn test_migrate_wallet_account_xpub_vanilla_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with wallet_account_xpub_vanilla + let wallet_account_xpub_vanilla = "tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyyN2h2kyXJsqJcK8Zz5yVzJAGqFqWyYSyMqvhzKQHQdD8A8JFYGKjzG8VzWJdK8BfMiHdF8J4gHh"; + let wallet_account_xpub_vanilla_file = temp_dir.path().join(WALLET_ACCOUNT_XPUB_VANILLA_FNAME); + fs::write(&wallet_account_xpub_vanilla_file, wallet_account_xpub_vanilla).unwrap(); + + // Migrate from file to DB + db_manager.migrate_wallet_account_xpub_vanilla_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("wallet_account_xpub_vanilla").await.unwrap(); + assert_eq!(loaded, Some(wallet_account_xpub_vanilla.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(wallet_account_xpub_vanilla_file.exists()); + let file_content = fs::read_to_string(&wallet_account_xpub_vanilla_file).unwrap(); + assert_eq!(file_content.trim(), wallet_account_xpub_vanilla); +} + +#[tokio::test] +async fn test_migrate_wallet_master_fingerprint_from_file() { + // Create a temporary directory + let temp_dir = TempDir::new().unwrap(); + let db_path = temp_dir.path().join("test.db"); + let db_manager = DatabaseManager::new(&db_path).await.unwrap(); + + // Create a file with wallet_master_fingerprint + let wallet_master_fingerprint = "master_fingerprint_123"; + let wallet_master_fingerprint_file = temp_dir.path().join(WALLET_MASTER_FINGERPRINT_FNAME); + fs::write(&wallet_master_fingerprint_file, wallet_master_fingerprint).unwrap(); + + // Migrate from file to DB + db_manager.migrate_wallet_master_fingerprint_from_file(temp_dir.path()).await.unwrap(); + + // Verify value is in DB + let loaded = db_manager.load_rgb_config("wallet_master_fingerprint").await.unwrap(); + assert_eq!(loaded, Some(wallet_master_fingerprint.to_string())); + + // Verify file still exists (migration doesn't delete it) + assert!(wallet_master_fingerprint_file.exists()); + let file_content = fs::read_to_string(&wallet_master_fingerprint_file).unwrap(); + assert_eq!(file_content.trim(), wallet_master_fingerprint); +} diff --git a/src/test/send_receive.rs b/src/test/send_receive.rs index d031fbc4..9f4c012c 100644 --- a/src/test/send_receive.rs +++ b/src/test/send_receive.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/send_receive/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn send_receive() { initialize(); diff --git a/src/test/swap_assets_liquidity_both_ways.rs b/src/test/swap_assets_liquidity_both_ways.rs index e22674ae..d4fbb07b 100644 --- a/src/test/swap_assets_liquidity_both_ways.rs +++ b/src/test/swap_assets_liquidity_both_ways.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_assets_liquidity_both_ways/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_assets_liquidity_both_ways() { initialize(); diff --git a/src/test/swap_reverse_same_channel.rs b/src/test/swap_reverse_same_channel.rs index 5d8a71a1..42058981 100644 --- a/src/test/swap_reverse_same_channel.rs +++ b/src/test/swap_reverse_same_channel.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_reverse_same_channel/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_reverse_same_channel() { initialize(); diff --git a/src/test/swap_roundtrip_assets.rs b/src/test/swap_roundtrip_assets.rs index e7d7c753..2b3bd1d1 100644 --- a/src/test/swap_roundtrip_assets.rs +++ b/src/test/swap_roundtrip_assets.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_assets/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_assets() { initialize(); diff --git a/src/test/swap_roundtrip_buy.rs b/src/test/swap_roundtrip_buy.rs index f3970436..23f856de 100644 --- a/src/test/swap_roundtrip_buy.rs +++ b/src/test/swap_roundtrip_buy.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_buy/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_buy() { initialize(); diff --git a/src/test/swap_roundtrip_buy_same_channel.rs b/src/test/swap_roundtrip_buy_same_channel.rs index 77d83e51..c0d15e1b 100644 --- a/src/test/swap_roundtrip_buy_same_channel.rs +++ b/src/test/swap_roundtrip_buy_same_channel.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_buy_same_channel/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_buy_same_channel() { initialize(); diff --git a/src/test/swap_roundtrip_fail_amount_maker.rs b/src/test/swap_roundtrip_fail_amount_maker.rs index 6975519e..e1720418 100644 --- a/src/test/swap_roundtrip_fail_amount_maker.rs +++ b/src/test/swap_roundtrip_fail_amount_maker.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_amount_maker/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "Not enough assets")] async fn swap_fail_amount_maker() { diff --git a/src/test/swap_roundtrip_fail_amount_taker.rs b/src/test/swap_roundtrip_fail_amount_taker.rs index f92a9ef2..2882494d 100644 --- a/src/test/swap_roundtrip_fail_amount_taker.rs +++ b/src/test/swap_roundtrip_fail_amount_taker.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_amount_taker/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "Not enough assets")] async fn swap_fail_amount_taker() { diff --git a/src/test/swap_roundtrip_fail_btc2btc.rs b/src/test/swap_roundtrip_fail_btc2btc.rs index 8b3bbc49..249787f2 100644 --- a/src/test/swap_roundtrip_fail_btc2btc.rs +++ b/src/test/swap_roundtrip_fail_btc2btc.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_btc2btc/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "cannot swap BTC for BTC")] async fn swap_roundtrip_fail_btc2btc() { diff --git a/src/test/swap_roundtrip_fail_invalid_asset_from.rs b/src/test/swap_roundtrip_fail_invalid_asset_from.rs index c4ffa4e3..ecad59d4 100644 --- a/src/test/swap_roundtrip_fail_invalid_asset_from.rs +++ b/src/test/swap_roundtrip_fail_invalid_asset_from.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_invalid_asset_from/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "Invalid asset ID")] async fn swap_fail_invalid_asset_from() { diff --git a/src/test/swap_roundtrip_fail_invalid_asset_to.rs b/src/test/swap_roundtrip_fail_invalid_asset_to.rs index 88fe8bdd..0b53b95b 100644 --- a/src/test/swap_roundtrip_fail_invalid_asset_to.rs +++ b/src/test/swap_roundtrip_fail_invalid_asset_to.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_invalid_asset_to/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "Invalid asset ID")] async fn swap_fail_invalid_asset_to() { diff --git a/src/test/swap_roundtrip_fail_same_asset.rs b/src/test/swap_roundtrip_fail_same_asset.rs index 0d315a07..d09a7392 100644 --- a/src/test/swap_roundtrip_fail_same_asset.rs +++ b/src/test/swap_roundtrip_fail_same_asset.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_same_asset/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] #[should_panic(expected = "cannot swap the same asset")] async fn swap_roundtrip_fail_same_asset() { diff --git a/src/test/swap_roundtrip_fail_timeout.rs b/src/test/swap_roundtrip_fail_timeout.rs index f18325f3..2e36129a 100644 --- a/src/test/swap_roundtrip_fail_timeout.rs +++ b/src/test/swap_roundtrip_fail_timeout.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_timeout/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_fail_timeout() { initialize(); diff --git a/src/test/swap_roundtrip_fail_whitelist.rs b/src/test/swap_roundtrip_fail_whitelist.rs index 638ff5b2..783f7702 100644 --- a/src/test/swap_roundtrip_fail_whitelist.rs +++ b/src/test/swap_roundtrip_fail_whitelist.rs @@ -11,7 +11,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_fail_whitelist/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_fail_whitelist() { initialize(); diff --git a/src/test/swap_roundtrip_multihop_asset_asset.rs b/src/test/swap_roundtrip_multihop_asset_asset.rs index dcac6f0e..de8290b6 100644 --- a/src/test/swap_roundtrip_multihop_asset_asset.rs +++ b/src/test/swap_roundtrip_multihop_asset_asset.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_multihop_asset_asset/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_multihop_asset_asset() { initialize(); diff --git a/src/test/swap_roundtrip_multihop_buy.rs b/src/test/swap_roundtrip_multihop_buy.rs index 61ba009e..3a337394 100644 --- a/src/test/swap_roundtrip_multihop_buy.rs +++ b/src/test/swap_roundtrip_multihop_buy.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_multihop_buy/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_multihop_buy() { initialize(); diff --git a/src/test/swap_roundtrip_multihop_sell.rs b/src/test/swap_roundtrip_multihop_sell.rs index e076d8e0..9d229c5e 100644 --- a/src/test/swap_roundtrip_multihop_sell.rs +++ b/src/test/swap_roundtrip_multihop_sell.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_multihop_sell/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_multihop_sell() { initialize(); diff --git a/src/test/swap_roundtrip_sell.rs b/src/test/swap_roundtrip_sell.rs index ebf6c71a..9202a96b 100644 --- a/src/test/swap_roundtrip_sell.rs +++ b/src/test/swap_roundtrip_sell.rs @@ -5,7 +5,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/swap_roundtrip_sell/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn swap_roundtrip_sell() { initialize(); diff --git a/src/test/upload_asset_media.rs b/src/test/upload_asset_media.rs index 641669ea..d8894f1a 100644 --- a/src/test/upload_asset_media.rs +++ b/src/test/upload_asset_media.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/upload_asset_media/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn success() { initialize(); @@ -52,7 +52,7 @@ async fn success() { } #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn fail() { initialize(); diff --git a/src/test/vanilla_payment_on_rgb_channel.rs b/src/test/vanilla_payment_on_rgb_channel.rs index e6fa3e5f..c24c4d5e 100644 --- a/src/test/vanilla_payment_on_rgb_channel.rs +++ b/src/test/vanilla_payment_on_rgb_channel.rs @@ -3,7 +3,7 @@ use super::*; const TEST_DIR_BASE: &str = "tmp/vanilla_payment_on_rgb_channel/"; #[serial_test::serial] -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 3)] #[traced_test] async fn vanilla_payment_on_rgb_channel() { initialize(); diff --git a/src/utils.rs b/src/utils.rs index c214f086..abdc6a49 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -19,7 +19,6 @@ use rgb_lib::{bdk_wallet::keys::bip39::Mnemonic, BitcoinNetwork, ContractId}; use std::{ collections::HashSet, fmt::Write, - fs, net::{SocketAddr, TcpStream, ToSocketAddrs}, path::Path, path::PathBuf, @@ -90,6 +89,7 @@ pub(crate) struct StaticState { pub(crate) ldk_data_dir: PathBuf, pub(crate) logger: Arc, pub(crate) max_media_upload_size_mb: u16, + pub(crate) database_manager: Arc, } pub(crate) struct UnlockedAppState { @@ -111,6 +111,7 @@ pub(crate) struct UnlockedAppState { pub(crate) rgb_send_lock: Arc>, pub(crate) channel_ids_map: Arc>, pub(crate) proxy_endpoint: String, + pub(crate) database_manager: Arc, } impl UnlockedAppState { @@ -156,8 +157,10 @@ impl Writeable for UserOnionMessageContents { } } -pub(crate) fn check_already_initialized(mnemonic_path: &Path) -> Result<(), APIError> { - if mnemonic_path.exists() { +pub(crate) async fn check_already_initialized( + database_manager: &crate::database::DatabaseManager, +) -> Result<(), APIError> { + if database_manager.check_already_initialized().await? { return Err(APIError::AlreadyInitialized); } Ok(()) @@ -172,20 +175,16 @@ pub(crate) fn check_password_strength(password: String) -> Result<(), APIError> Ok(()) } -pub(crate) fn check_password_validity( +pub(crate) async fn check_password_validity( password: &str, - storage_dir_path: &Path, + database_manager: &crate::database::DatabaseManager, ) -> Result { - let mnemonic_path = get_mnemonic_path(storage_dir_path); - if let Ok(encrypted_mnemonic) = fs::read_to_string(mnemonic_path) { - let mcrypt = new_magic_crypt!(password, 256); - let mnemonic_str = mcrypt - .decrypt_base64_to_string(encrypted_mnemonic) - .map_err(|_| APIError::WrongPassword)?; - Ok(Mnemonic::from_str(&mnemonic_str).expect("valid mnemonic")) - } else { - Err(APIError::NotInitialized) - } + let encrypted_mnemonic = database_manager.load_mnemonic().await?; + let mcrypt = new_magic_crypt!(password, 256); + let mnemonic_str = mcrypt + .decrypt_base64_to_string(encrypted_mnemonic) + .map_err(|_| APIError::WrongPassword)?; + Ok(Mnemonic::from_str(&mnemonic_str).expect("valid mnemonic")) } pub(crate) fn check_channel_id(channel_id_str: &str) -> Result { @@ -206,27 +205,16 @@ pub(crate) fn check_port_is_available(port: u16) -> Result<(), AppError> { Ok(()) } -pub(crate) fn get_mnemonic_path(storage_dir_path: &Path) -> PathBuf { - storage_dir_path.join("mnemonic") -} - -pub(crate) fn encrypt_and_save_mnemonic( +pub(crate) async fn encrypt_and_save_mnemonic( password: String, mnemonic: String, - mnemonic_path: &Path, + database_manager: &crate::database::DatabaseManager, ) -> Result<(), APIError> { let mcrypt = new_magic_crypt!(password, 256); let encrypted_mnemonic = mcrypt.encrypt_str_to_base64(mnemonic); - match fs::write(mnemonic_path, encrypted_mnemonic) { - Ok(()) => { - tracing::info!("Created a new wallet"); - Ok(()) - } - Err(e) => Err(APIError::FailedKeysCreation( - mnemonic_path.to_string_lossy().to_string(), - e.to_string(), - )), - } + database_manager.save_mnemonic(encrypted_mnemonic).await?; + tracing::info!("Created a new wallet"); + Ok(()) } pub(crate) async fn connect_peer_if_necessary( @@ -352,6 +340,33 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr let cancel_token = CancellationToken::new(); + let db_path = args.storage_dir_path.join("rln_db"); + let database_manager = Arc::new(crate::database::DatabaseManager::new(&db_path).await?); + + // Migrate existing RGB config from files to database on startup + // and sync database values back to files for rust-lightning compatibility + database_manager + .migrate_indexer_url_from_file(&args.storage_dir_path) + .await?; + database_manager + .migrate_bitcoin_network_from_file(&args.storage_dir_path) + .await?; + database_manager + .migrate_wallet_fingerprint_from_file(&args.storage_dir_path) + .await?; + database_manager + .migrate_wallet_account_xpub_colored_from_file(&args.storage_dir_path) + .await?; + database_manager + .migrate_wallet_account_xpub_vanilla_from_file(&args.storage_dir_path) + .await?; + database_manager + .migrate_wallet_master_fingerprint_from_file(&args.storage_dir_path) + .await?; + database_manager + .sync_rgb_config_to_files(&args.storage_dir_path) + .await?; + let static_state = Arc::new(StaticState { ldk_peer_listening_port: args.ldk_peer_listening_port, network: args.network, @@ -359,6 +374,7 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr ldk_data_dir, logger, max_media_upload_size_mb: args.max_media_upload_size_mb, + database_manager, }); let app_state = Arc::new(AppState { @@ -371,9 +387,13 @@ pub(crate) async fn start_daemon(args: &UserArgs) -> Result, AppEr revoked_tokens: Arc::new(Mutex::new(HashSet::new())), }); - // Load revoked tokens from file if authentication is enabled + // Load revoked tokens from database if authentication is enabled if app_state.root_public_key.is_some() { - let loaded_tokens = app_state.load_revoked_tokens()?; + let loaded_tokens = app_state + .static_state + .database_manager + .load_revoked_tokens() + .await?; *app_state.revoked_tokens.lock().unwrap() = loaded_tokens; }