From badf52ce4bc3eb9429fe769d43a23292009efe30 Mon Sep 17 00:00:00 2001 From: Nicolas80 <nicolas.pmail@protonmail.com> Date: Sat, 28 Dec 2024 18:32:19 +0100 Subject: [PATCH] Adding db persistence for all SecretFormat of vault keys as well as supporting derivations * Added "/.idea" exclusion in .gitignore (for when using JetBrains IDEs) * Added dialoguer dependency for easier user input handling (see in inputs.rs) * Added sea-orm dependency to allow having DB entity mappings and use a local sqlite file database * Added rstest test dependency for parameterized tests support * Added derivation tests for each SecretFormat (including cesium v1 key derivation, using sp_core::ed25519::Pair) * Made a lot of changes to add vault_account and vault_derivation db tables to persist vault keys & derivations * Added support for KeyPair::Ed25519 linking to sp_core::ed25519::Pair which can be created from secret seed retrieved from nacl::sign::Keypair (which is created from cesium id + secret) ** This was necessary to allow deriving keys from "cesium v1" keys (to be reviewed - it might be a bad idea to permit that from a security point of view) * Only kept original (substrate) keyfiles support for migration (use "vault list-files" and "vault migrate") * Added possibility to give either "-a" Address or "-v" Vault Name as general option * Added extra commands in Vault ** list-files: (deprecated)List available key files (needs to be migrated with command "vault migrate" in order to use them) ** migrate: (deprecated)Migrate old key files into db (will have to provide password for each key) ** 'list' now has sub-commands 'all' or 'root' to show all keys or only root keys (without derivation path) ** use: "Use specific vault key (changes the config address)", which will have the same behaviour as `gcli <-a <Address>|-v <VaultName>> config save` (left a FIXME in there to review) ** derivation: Add a derivation to an existing (root) vault key ** rename: Give a meaningful vault name to a vault key or derivation ** remove: Remove a vault key (and potential derivations if it's a root key) * Had to bubble up "await" and "async" in a lot of places * ... --- .gitignore | 1 + Cargo.lock | 1185 ++++++++++++++++++++++++++++-- Cargo.toml | 5 + src/commands/identity.rs | 18 +- src/commands/net_test.rs | 4 +- src/commands/revocation.rs | 8 +- src/commands/vault.rs | 976 ++++++++++++++++++++++-- src/conf.rs | 21 +- src/data.rs | 65 +- src/database.rs | 71 ++ src/entities.rs | 2 + src/entities/vault_account.rs | 106 +++ src/entities/vault_derivation.rs | 150 ++++ src/inputs.rs | 85 +++ src/keys.rs | 461 +++++++++++- src/main.rs | 58 +- src/utils.rs | 25 +- 17 files changed, 3067 insertions(+), 174 deletions(-) create mode 100644 src/database.rs create mode 100644 src/entities.rs create mode 100644 src/entities/vault_account.rs create mode 100644 src/entities/vault_derivation.rs create mode 100644 src/inputs.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..40d9aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 574da92..b94f02f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,17 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -111,6 +122,12 @@ 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.18" @@ -571,7 +588,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.37", + "rustix 0.38.42", "slab", "tracing", "windows-sys 0.59.0", @@ -614,7 +631,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.1", "futures-lite", - "rustix 0.38.37", + "rustix 0.38.42", "tracing", ] @@ -630,12 +647,34 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.37", + "rustix 0.38.42", "signal-hook-registry", "slab", "windows-sys 0.59.0", ] +[[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.87", +] + [[package]] name = "async-task" version = "4.7.1" @@ -650,7 +689,16 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", ] [[package]] @@ -767,6 +815,20 @@ dependencies = [ "serde", ] +[[package]] +name = "bigdecimal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "bincode" version = "1.3.3" @@ -822,6 +884,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -896,6 +961,29 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "bounded-collections" version = "0.2.0" @@ -929,6 +1017,28 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" +[[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" @@ -1001,6 +1111,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "serde", "windows-targets 0.52.6", ] @@ -1046,7 +1157,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1103,7 +1214,7 @@ dependencies = [ "crossterm 0.27.0", "strum", "strum_macros", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -1149,6 +1260,19 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1231,6 +1355,21 @@ dependencies = [ "serde", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +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 = "crc32fast" version = "1.4.2" @@ -1375,7 +1514,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1423,7 +1562,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1445,7 +1584,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1468,9 +1607,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1490,7 +1640,7 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1501,7 +1651,7 @@ checksum = "62d671cc41a825ebabc75757b62d3d168c577f9149b2d49ece1dad1f72119d25" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1514,7 +1664,20 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.79", + "syn 2.0.87", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", ] [[package]] @@ -1587,7 +1750,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1627,12 +1790,18 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.79", + "syn 2.0.87", "termcolor", "toml 0.8.19", "walkdir", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1725,6 +1894,9 @@ name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -1746,6 +1918,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1782,12 +1960,23 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", ] [[package]] @@ -1833,7 +2022,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -1962,12 +2151,38 @@ dependencies = [ "thiserror", ] +[[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 = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2058,6 +2273,17 @@ dependencies = [ "num_cpus", ] +[[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.30" @@ -2085,7 +2311,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2136,6 +2362,7 @@ dependencies = [ "colored", "comfy-table", "confy", + "dialoguer", "directories 5.0.1", "env_logger", "futures", @@ -2147,7 +2374,9 @@ dependencies = [ "parity-scale-codec", "reqwest", "rpassword", + "rstest", "scrypt", + "sea-orm", "serde", "serde_json", "sp-core", @@ -2205,6 +2434,12 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "graphql-introspection-query" version = "0.2.0" @@ -2314,6 +2549,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" @@ -2321,7 +2559,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] @@ -2330,7 +2568,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", "serde", ] @@ -2341,6 +2579,15 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -2416,6 +2663,15 @@ dependencies = [ "hmac 0.8.1", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "http" version = "0.2.12" @@ -2559,7 +2815,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.79", + "syn 2.0.87", "unic-langid", ] @@ -2573,7 +2829,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -2671,6 +2927,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" +[[package]] +name = "inherent" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "inout" version = "0.1.3" @@ -2693,7 +2960,7 @@ dependencies = [ "newline-converter", "thiserror", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -3034,12 +3301,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" -version = "0.2.159" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libm" @@ -3105,6 +3375,17 @@ dependencies = [ "libsecp256k1-core", ] +[[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.1.4" @@ -3169,6 +3450,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -3181,7 +3472,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" dependencies = [ - "rustix 0.38.37", + "rustix 0.38.42", ] [[package]] @@ -3265,6 +3556,23 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30aefc44d813c51b5e7952950e87c17f2e0e1a3274d63c8281a701e05323d548" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "newline-converter" version = "0.2.2" @@ -3322,6 +3630,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -3341,6 +3672,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -3359,6 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3408,17 +3751,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "openssl" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] [[package]] -name = "option-ext" -version = "0.2.0" +name = "openssl-macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ouroboros" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +dependencies = [ + "heck 0.4.1", + "itertools 0.12.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.87", +] + [[package]] name = "overload" version = "0.1.1" @@ -3522,6 +3937,15 @@ dependencies = [ "password-hash", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3545,7 +3969,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3571,6 +3995,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3581,6 +4016,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "polkavm-common" version = "0.9.0" @@ -3605,7 +4046,7 @@ dependencies = [ "polkavm-common", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3615,7 +4056,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ba81f7b5faac81e528eb6158a6f3c9e0bb1008e0ffa19653bc8dea925ecb429" dependencies = [ "polkavm-derive-impl", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3628,7 +4069,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.37", + "rustix 0.38.42", "tracing", "windows-sys 0.59.0", ] @@ -3650,6 +4091,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.20" @@ -3666,7 +4113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3725,6 +4172,28 @@ dependencies = [ "version_check", ] +[[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.87", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -3734,6 +4203,19 @@ 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.87", + "version_check", + "yansi", +] + [[package]] name = "psm" version = "0.1.23" @@ -3743,6 +4225,26 @@ dependencies = [ "cc", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.37" @@ -3861,7 +4363,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -3908,6 +4410,21 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -3991,6 +4508,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rpassword" version = "7.3.1" @@ -4002,6 +4548,56 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.87", + "unicode-ident", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -4032,7 +4628,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.79", + "syn 2.0.87", "walkdir", ] @@ -4046,6 +4642,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec 0.7.6", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -4089,15 +4701,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4391,7 +5003,7 @@ dependencies = [ "proc-macro2", "quote", "scale-info", - "syn 2.0.79", + "syn 2.0.87", "thiserror", ] @@ -4431,7 +5043,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9a8ef13a93c54d20580de1e5c413e624e53121d42fc7e2c11d10ef7f8b02367" dependencies = [ - "ahash", + "ahash 0.8.11", "cfg-if", "hashbrown 0.13.2", ] @@ -4482,6 +5094,99 @@ dependencies = [ "untrusted", ] +[[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.87", +] + +[[package]] +name = "sea-orm" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b24d72a69e89762982c29af249542b06c59fa131f87cc9d5b94be1f692b427a" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0497f4fd82ecb2a222bea5319b9048f8ab58d4e734d095b062987acbcdeecdda" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.87", + "unicode-ident", +] + +[[package]] +name = "sea-query" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085e94f7d7271c0393ac2d164a39994b1dff1b06bc40cd9a0da04f3d672b0fee" +dependencies = [ + "bigdecimal", + "chrono", + "inherent", + "ordered-float", + "rust_decimal", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "sea-query-binder" +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]] +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" @@ -4595,7 +5300,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -4708,6 +5413,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -4754,6 +5465,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple-mermaid" version = "0.1.1" @@ -4780,6 +5497,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "smol" @@ -5063,7 +5783,7 @@ source = "git+https://github.com/duniter/duniter-polkadot-sdk#c84530c57a6f9ab808 dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5073,7 +5793,7 @@ source = "git+https://github.com/duniter/duniter-polkadot-sdk.git?branch=duniter dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5217,7 +5937,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5230,7 +5950,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5316,7 +6036,7 @@ name = "sp-trie" version = "29.0.0" source = "git+https://github.com/duniter/duniter-polkadot-sdk.git?branch=duniter-substrate-v1.14.0#bcc60f3e4170c3908689252242f40761270c9a51" dependencies = [ - "ahash", + "ahash 0.8.11", "hash-db", "lazy_static", "memory-db", @@ -5376,6 +6096,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -5387,6 +6110,230 @@ dependencies = [ "der", ] +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" +dependencies = [ + "atoi", + "bigdecimal", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.3.1", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.14.5", + "hashlink", + "hex", + "indexmap 2.6.0", + "log", + "memchr", + "native-tls", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.87", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.87", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.6.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac 0.12.1", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "rust_decimal", + "serde", + "sha1", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" +dependencies = [ + "atoi", + "base64 0.22.1", + "bigdecimal", + "bitflags 2.6.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac 0.12.1", + "home", + "itoa", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "rand", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "time", + "tracing", + "url", + "uuid", +] + [[package]] name = "ss58-registry" version = "1.50.0" @@ -5414,6 +6361,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.10.0" @@ -5442,7 +6400,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5513,7 +6471,7 @@ dependencies = [ "scale-info", "scale-typegen", "subxt-metadata", - "syn 2.0.79", + "syn 2.0.87", "thiserror", "tokio", ] @@ -5573,7 +6531,7 @@ dependencies = [ "quote", "scale-typegen", "subxt-codegen", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5601,9 +6559,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -5649,6 +6607,19 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix 0.38.42", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -5675,7 +6646,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5688,6 +6659,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -5736,7 +6738,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -5897,7 +6899,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -6088,6 +7090,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -6100,12 +7108,24 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "universal-hash" version = "0.5.1" @@ -6148,12 +7168,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "serde", +] + [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[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" @@ -6215,6 +7250,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.93" @@ -6237,7 +7278,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -6271,7 +7312,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6489,6 +7530,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6792,6 +7843,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yap" version = "0.11.0" @@ -6816,7 +7873,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] [[package]] @@ -6836,5 +7893,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.87", ] diff --git a/Cargo.toml b/Cargo.toml index efb44d9..272280b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ reqwest = { version = "^0.11.27", default-features = false, features = [ "rustls-tls", ] } rpassword = "^7.3.1" +dialoguer = "0.11.0" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0.128" tokio = { version = "^1.40.0", features = ["macros"] } @@ -45,6 +46,7 @@ bs58 = "^0.5.1" inquire = "^0.6.2" directories = "^5.0.1" comfy-table = "^7.1.1" +sea-orm = { version = "1.1.0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] } # crypto scrypt = { version = "^0.11", default-features = false } # for old-style key generation @@ -54,6 +56,9 @@ age = { default-features = false, version = "^0.10.0", features = ["armor"] } bip39 = { version = "^2.0.0", features = ["rand"] } # mnemonic colored = "2.1.0" +# Tests +rstest = "0.23.0" + # allows to build gcli for different runtimes and with different predefined networks [features] default = ["gdev"] # default runtime is "gdev", gdev network is available diff --git a/src/commands/identity.rs b/src/commands/identity.rs index 0d8bfc8..91299fd 100644 --- a/src/commands/identity.rs +++ b/src/commands/identity.rs @@ -124,7 +124,7 @@ pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliE } Subcommand::GenRevocDoc => { data = data.fetch_idty_index().await?; - commands::revocation::print_revoc_sig(&data) + commands::revocation::print_revoc_sig(&data).await } Subcommand::MemberCount => { println!( @@ -488,7 +488,7 @@ pub async fn confirm_identity(data: &Data, name: String) -> Result<(), subxt::Er /// generate revokation document and submit it immediately pub async fn revoke_identity(data: &Data) -> Result<(), subxt::Error> { - let (_payload, signature) = generate_revoc_doc(data); + let (_payload, signature) = generate_revoc_doc(data).await; // Transform signature to MultiSignature // TODO: allow other signature formats @@ -519,6 +519,11 @@ pub fn generate_link_account( let signature = keypair.sign(&payload); (payload, Signature::Sr25519(signature)) } + KeyPair::Ed25519(keypair) => { + let signature = keypair.sign(&payload); + (payload, Signature::Ed25519(signature)) + } + //FIXME Cleanup KeyPair::Nacl(keypair) => { let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); (payload, Signature::Nacl(signature)) @@ -545,6 +550,11 @@ pub fn generate_chok_payload( let signature = keypair.sign(&payload); (payload, Signature::Sr25519(signature)) } + KeyPair::Ed25519(keypair) => { + let signature = keypair.sign(&payload); + (payload, Signature::Ed25519(signature)) + } + //FIXME Cleanup KeyPair::Nacl(keypair) => { // should not migrate to Nacl let signature = nacl::sign::signature(&payload, &keypair.skey).expect("could not sign"); @@ -564,6 +574,8 @@ pub async fn link_account( // TODO cleaner way to manage signature let signature = match signature { Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()), + Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()), + //FIXME Cleanup Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()), }; @@ -588,6 +600,8 @@ pub async fn change_owner_key( // TODO cleaner way to manage signature let signature = match signature { Signature::Sr25519(signature) => MultiSignature::Sr25519(signature.into()), + Signature::Ed25519(signature) => MultiSignature::Ed25519(signature.into()), + //FIXME Cleanup Signature::Nacl(signature) => MultiSignature::Ed25519(signature.try_into().unwrap()), }; diff --git a/src/commands/net_test.rs b/src/commands/net_test.rs index 6f1ac34..f9cea93 100644 --- a/src/commands/net_test.rs +++ b/src/commands/net_test.rs @@ -4,7 +4,7 @@ use sp_core::DeriveJunction; use subxt::ext::sp_runtime::MultiAddress; pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> anyhow::Result<()> { - let KeyPair::Sr25519(keypair) = data.keypair() else { + let KeyPair::Sr25519(keypair) = data.keypair().await else { panic!("Cesium keys not implemented there") }; let mut pairs = Vec::new(); @@ -43,7 +43,7 @@ pub async fn repart(data: &Data, target: u32, actual_repart: Option<u32>) -> any } pub async fn spam_roll(data: &Data, actual_repart: usize) -> anyhow::Result<()> { - let KeyPair::Sr25519(keypair) = data.keypair() else { + let KeyPair::Sr25519(keypair) = data.keypair().await else { panic!("Cesium keys not implemented there") }; let client = data.client(); diff --git a/src/commands/revocation.rs b/src/commands/revocation.rs index d9d60e1..aec5690 100644 --- a/src/commands/revocation.rs +++ b/src/commands/revocation.rs @@ -4,15 +4,15 @@ use crate::*; // use crate::runtime::runtime_types::pallet_identity::types::RevocationPayload; type EncodedRevocationPayload = Vec<u8>; -pub fn print_revoc_sig(data: &Data) { - let (_, signature) = generate_revoc_doc(data); +pub async fn print_revoc_sig(data: &Data) { + let (_, signature) = generate_revoc_doc(data).await; println!("revocation payload signature"); println!("0x{}", hex::encode(signature)); } -pub fn generate_revoc_doc(data: &Data) -> (EncodedRevocationPayload, sr25519::Signature) { +pub async fn generate_revoc_doc(data: &Data) -> (EncodedRevocationPayload, sr25519::Signature) { let payload = (b"revo", data.genesis_hash, data.idty_index()).encode(); - let KeyPair::Sr25519(keypair) = data.keypair() else { + let KeyPair::Sr25519(keypair) = data.keypair().await else { panic!("Cesium keys not implemented there") }; let signature = keypair.sign(&payload); diff --git a/src/commands/vault.rs b/src/commands/vault.rs index b6e09b7..1478b67 100644 --- a/src/commands/vault.rs +++ b/src/commands/vault.rs @@ -1,19 +1,73 @@ +use crate::entities::vault_account::CryptoType; +use crate::entities::{vault_account, vault_derivation}; use crate::*; use age::secrecy::Secret; +use comfy_table::{Cell, Table}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, EntityTrait, ModelTrait}; +use sea_orm::{ColumnTrait, QueryFilter}; +use sea_orm::{ConnectionTrait, TransactionTrait}; use std::io::{Read, Write}; +use std::path::PathBuf; /// define universal dividends subcommands -#[derive(Clone, Default, Debug, clap::Parser)] +#[derive(Clone, Debug, clap::Parser)] pub enum Subcommand { - #[default] /// List available keys - List, - /// Show where vault stores secret - Where, + #[clap(subcommand)] + List(ListChoice), + /// Use specific vault key (changes the config address) + Use { + #[clap(flatten)] + address_or_vault_name: AddressOrVaultNameGroup, + }, /// Generate a mnemonic Generate, - /// Import mnemonic with interactive prompt - Import, + /// Import key from (substrate)mnemonic or other format with interactive prompt + Import { + /// Secret key format (substrate, seed, cesium) + #[clap(short = 'S', long, required = false, default_value = SecretFormat::Substrate)] + secret_format: SecretFormat, + }, + /// Add a derivation to an existing (root) vault key + #[clap(alias = "deriv")] + #[clap(alias = "derive")] + Derivation { + #[clap(flatten)] + address_or_vault_name: AddressOrVaultNameGroup, + }, + /// Give a meaningful vault name to a vault key or derivation + Rename { + /// Account Address + address: AccountId, + }, + /// Remove a vault key (and potential derivations if it's a root key) + Remove { + #[clap(flatten)] + address_or_vault_name: AddressOrVaultNameGroup, + }, + /// (deprecated)List available key files (needs to be migrated with command "vault migrate" in order to use them) + ListFiles, + /// (deprecated)Migrate old key files into db (will have to provide password for each key) + Migrate, + /// Show where vault db (or old keys) is stored + Where, +} + +#[derive(Clone, Default, Debug, clap::Parser)] +pub enum ListChoice { + /// List all keys and derivations in the vault + #[default] + All, + /// List only root keys + Root, +} + +pub struct VaultDataToImport { + secret_format: SecretFormat, + secret: keys::Secret, + address: AccountId, + key_pair: KeyPair, } // encrypt input with passphrase @@ -43,76 +97,898 @@ fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptErro } /// handle ud commands -pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { +pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { // match subcommand match command { - Subcommand::List => { - if let Ok(entries) = std::fs::read_dir(data.project_dir.data_dir()) { + Subcommand::List(choice) => match choice { + ListChoice::All => { + let derivations = vault_derivation::list_all_derivations_in_order( + data.connection.as_ref().unwrap(), + ) + .await?; + + let table = compute_vault_derivations_table(&derivations).await?; + println!("available keys:"); - entries.for_each(|e| println!("{}", e.unwrap().file_name().to_str().unwrap())); - } else { - println!("could not read project dir"); + println!("{table}"); + } + ListChoice::Root => { + let derivations = vault_derivation::list_all_root_derivations_in_order( + data.connection.as_ref().unwrap(), + ) + .await?; + + let table = compute_vault_derivations_table(&derivations).await?; + + println!("available root keys:"); + println!("{table}"); } + }, + Subcommand::ListFiles => { + let vault_key_addresses = fetch_vault_key_addresses(&data).await?; + + let table = compute_vault_key_files_table(&vault_key_addresses).await?; + + println!("available key files (needs to be migrated with command \"vault migrate\" in order to use them):"); + println!("{table}"); } - Subcommand::Where => { - println!("{}", data.project_dir.data_dir().to_str().unwrap()); + Subcommand::Use { + address_or_vault_name, + } => { + let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; + + //FIXME not sure if this is ok (but since it's a CLI; this data instance won't be used afterwards) + let mut data = data; + data.cfg.address = + Some(AccountId::from_str(&derivation.address).expect("invalid address")); + + println!("Using key {}", derivation); + conf::save_config(&data); } Subcommand::Generate => { // TODO allow custom word count let mnemonic = bip39::Mnemonic::generate(12).unwrap(); println!("{mnemonic}"); } - Subcommand::Import => { - let mnemonic = rpassword::prompt_password("Mnemonic: ")?; + Subcommand::Import { secret_format } => { + let vault_data_for_import = + prompt_secret_and_compute_vault_data_to_import(secret_format)?; + + println!( + "Trying to import for address :'{}'", + vault_data_for_import.address + ); + + if let Some(derivation) = + vault_derivation::Entity::find_by_id(vault_data_for_import.address.to_string()) + .one(data.connection.as_ref().unwrap()) + .await? + { + println!( + "Vault entry already exists for address:'{}'", + vault_data_for_import.address + ); + + let linked_derivations = vault_derivation::fetch_all_linked_derivations_in_order( + data.connection.as_ref().unwrap(), + &derivation.root_address.clone(), + ) + .await?; + println!("Here are all the linked derivations already present in the vault:"); + + let table = compute_vault_derivations_table(&linked_derivations).await?; + println!("{table}"); + + return Ok(()); + } + println!("Enter password to protect the key"); - let password = rpassword::prompt_password("Password: ")?; - let address = store_mnemonic(&data, &mnemonic, password)?; - println!("Stored secret for {address}"); + let password = inputs::prompt_password_confirm()?; + + println!("(Optional) Enter a name for the vault entry"); + let name = inputs::prompt_vault_name()?; + + let txn = data.connection.as_ref().unwrap().begin().await?; + + let _derivation = create_derivation_for_vault_data_to_import( + &txn, + &vault_data_for_import, + &password, + name.as_ref(), + ) + .await?; + + txn.commit().await?; + + println!("Import done"); + } + Subcommand::Derivation { + address_or_vault_name, + } => { + let root_derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; + + if root_derivation.path.is_some() { + println!("Can only add derivation on a ROOT key."); + println!( + "The selected key with address:'{}' already has a ROOT with address:'{}'", + root_derivation.address, root_derivation.root_address + ); + println!("You can check for available ROOT keys with command 'vault list root'"); + return Ok(()); + } + + let vault_account = vault_account::Entity::find_by_id(&root_derivation.address) + .one(data.connection.as_ref().unwrap()) + .await? + .ok_or(GcliError::Input(format!( + "Could not find (root) vault account for address:'{}'", + root_derivation.address + )))?; + + println!("Adding derivation to root key: {root_derivation}"); + + println!("Enter password to decrypt the root key"); + + let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?; + + let derivation_path = inputs::prompt_vault_derivation_path()?; + + let derivation_secret_suri = format!("{root_secret_suri}{derivation_path}"); + + let derivation_keypair = + compute_keypair(vault_account.crypto_type, &derivation_secret_suri)?; + + let derivation_address: String = derivation_keypair.address().to_string(); + + let check_derivation = vault_derivation::Entity::find_by_id(&derivation_address) + .one(data.connection.as_ref().unwrap()) + .await?; + + if check_derivation.is_some() { + println!("Derivation already exists for address:'{derivation_address}'"); + return Ok(()); + } + + println!("(Optional) Enter a name for the new derivation"); + let name = inputs::prompt_vault_name()?; + + let derivation = vault_derivation::ActiveModel { + address: Set(derivation_address), + name: Set(name), + path: Set(Some(derivation_path)), + root_address: Set(root_derivation.root_address.clone()), + }; + let derivation = derivation.insert(data.connection.as_ref().unwrap()).await?; + println!("Created derivation {}", derivation); + } + Subcommand::Rename { address } => { + let derivation = vault_derivation::Entity::find_by_id(address.to_string()) + .one(data.connection.as_ref().unwrap()) + .await?; + + if derivation.is_none() { + println!("No vault entry found for address:'{address}'"); + println!("You might want to import it first with 'vault import'"); + return Ok(()); + } + + let derivation = derivation.unwrap(); + + println!( + "Current name for address:'{address}' is {:?}", + derivation.name + ); + + println!("Enter new vault name for the key (leave empty to remove the name)"); + let name = inputs::prompt_vault_name()?; + + let old_name = derivation.name.clone(); + let mut derivation: vault_derivation::ActiveModel = derivation.into(); + derivation.name = Set(name.clone()); + let _derivation = derivation.update(data.connection.as_ref().unwrap()).await?; + println!( + "Renamed address:'{address}' from {:?} to {:?}", + old_name, name + ); + } + Subcommand::Remove { + address_or_vault_name, + } => { + let derivation = retrieve_vault_derivation(&data, address_or_vault_name).await?; + let address_to_delete = derivation.address.clone(); + + let txn = data.connection.as_ref().unwrap().begin().await?; + + //If deleting a root derivation; also delete the vault account and all linked derivations + if derivation.path.is_none() { + let all_derivations_to_delete = + vault_derivation::fetch_all_linked_derivations_in_order( + &txn, + &address_to_delete, + ) + .await?; + + let table = compute_vault_derivations_table(&all_derivations_to_delete).await?; + + println!("All derivations linked to the root derivation:"); + println!("{table}"); + + println!( + "This root derivation has {} derivations in total", + all_derivations_to_delete.len() + ); + let confirmed = inputs::confirm_action( + "Are you sure you want to delete it along with the vault account (and saved key)?".to_string() + )?; + + if !confirmed { + return Ok(()); + } + + for derivation_to_delete in all_derivations_to_delete { + let delete_result = derivation_to_delete.delete(&txn).await?; + println!("Deleted {} derivation", delete_result.rows_affected); + } + + let delete_result = vault_account::Entity::delete_by_id(&address_to_delete) + .exec(&txn) + .await?; + println!("Deleted {} vault account", delete_result.rows_affected); + } else { + let delete_result = derivation.delete(&txn).await?; + println!("Deleted {} derivation", delete_result.rows_affected); + } + + txn.commit().await?; + + println!("Done removing address:'{address_to_delete}'"); + } + Subcommand::Migrate => { + println!("Migrating existing key files to db"); + + let vault_key_addresses = fetch_vault_key_addresses(&data).await?; + + let table = compute_vault_key_files_table(&vault_key_addresses).await?; + println!("available key files to possibly migrate:"); + println!("{table}"); + + for address in vault_key_addresses { + //Check if we already have a vault_derivation for that address + let derivation = vault_derivation::Entity::find_by_id(&address) + .one(data.connection.as_ref().unwrap()) + .await?; + + if derivation.is_some() { + //Already migrated + continue; + } + + println!(); + println!("Trying to migrate key {address}"); + let vault_data_from_file = match try_fetch_vault_data_from_file(&data, &address) { + Ok(Some(vault_data)) => vault_data, + Ok(None) => { + println!("No vault entry file found for address {address}"); + continue; + } + Err(e) => { + println!("Error while fetching vault data for address {address}: {e}"); + println!("Continuing to next one"); + continue; + } + }; + + let vault_data_to_import = VaultDataToImport { + secret_format: SecretFormat::Substrate, + secret: keys::Secret::SimpleSecret(vault_data_from_file.secret), + address: AccountId::from_str(&address).expect("invalid address"), + key_pair: vault_data_from_file.key_pair, + }; + + let txn = data.connection.as_ref().unwrap().begin().await?; + + let derivation = create_derivation_for_vault_data_to_import( + &txn, + &vault_data_to_import, + &vault_data_from_file.password, + None, + ) + .await?; + + txn.commit().await?; + println!("Import done: {}", derivation); + } + + println!("Migration done"); + } + Subcommand::Where => { + println!("{}", data.project_dir.data_dir().to_str().unwrap()); } }; Ok(()) } -/// store mnemonic protected with password -pub fn store_mnemonic( +fn parse_prefix_and_derivation_path_from_string( + raw_string: String, +) -> Result<(String, Option<String>), GcliError> { + if raw_string.contains("/") { + raw_string + .find("/") + .map_or(Err(GcliError::Input("Invalid format".to_string())), |idx| { + let (prefix, derivation_path) = raw_string.split_at(idx); + Ok((prefix.to_string(), Some(derivation_path.to_string()))) + }) + } else { + Ok((raw_string, None)) + } +} + +fn map_secret_format_to_crypto_type(secret_format: SecretFormat) -> CryptoType { + match secret_format { + SecretFormat::Seed => vault_account::CryptoType::Sr25519Seed, + SecretFormat::Substrate => vault_account::CryptoType::Sr25519Mnemonic, + SecretFormat::Predefined => vault_account::CryptoType::Sr25519Mnemonic, + SecretFormat::Cesium => vault_account::CryptoType::Ed25519Seed, + } +} + +/// This method will scan files in the data directory and return the addresses of the vault keys found +async fn fetch_vault_key_addresses(data: &Data) -> Result<Vec<String>, GcliError> { + let mut entries = std::fs::read_dir(data.project_dir.data_dir())? + .map(|res| res.map(|e| e.path())) + .collect::<Result<Vec<_>, std::io::Error>>()?; + + // To have consistent ordering + entries.sort(); + + let mut vault_key_addresses: Vec<String> = vec![]; + entries.iter().for_each(|dir_path| { + let filename = dir_path.file_name().unwrap().to_str().unwrap(); + // To only keep the address part of the filename for names like "<ss58 address>-<secret_format>" + let potential_address = filename.split("-").next().unwrap(); + // If potential_address is a valid AccountId + if AccountId::from_str(potential_address).is_ok() { + vault_key_addresses.push(potential_address.to_string()); + } + }); + + Ok(vault_key_addresses) +} + +async fn compute_vault_key_files_table(vault_key_addresses: &[String]) -> Result<Table, GcliError> { + let mut table = Table::new(); + table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); + table.set_header(vec!["Key file"]); + + vault_key_addresses.iter().for_each(|address| { + table.add_row(vec![Cell::new(address)]); + }); + + Ok(table) +} + +async fn compute_vault_derivations_table( + derivations_ordered: &[vault_derivation::Model], +) -> Result<Table, GcliError> { + let mut table = Table::new(); + table.load_preset(comfy_table::presets::UTF8_BORDERS_ONLY); + table.set_header(vec!["Address", "Path", "Vault Name"]); + let empty_name = "".to_string(); + let root_path = "<Root>".to_string(); + + let mut current_root_address = "".to_string(); + let mut current_root_name: Option<String> = None; + + for derivation in derivations_ordered { + if derivation.root_address != current_root_address { + // First entry should be a root derivation + if derivation.path.is_some() { + return Err(GcliError::Input( + "Order of derivations parameter is wrong".to_string(), + )); + } + current_root_address = derivation.root_address.clone(); + current_root_name = derivation.name.clone(); + } else { + // Validate that the RootAddress is the same as current_root_address + if derivation.root_address != current_root_address { + return Err(GcliError::Input( + "Order of derivations parameter is wrong".to_string(), + )); + } + } + + let address = if derivation.path.is_none() { + derivation.address.clone() + } else { + " ".to_string() + &derivation.address + }; + + let path = if derivation.path.is_none() { + root_path.clone() + } else { + derivation.path.clone().unwrap() + }; + + let name = if derivation.name.is_none() { + if derivation.path.is_none() { + empty_name.clone() + } else if let Some(current_root_name) = ¤t_root_name { + format!( + "<{}{}>", + current_root_name, + derivation.path.clone().unwrap() + ) + } else { + empty_name.clone() + } + } else { + derivation.name.clone().unwrap() + }; + + table.add_row(vec![ + Cell::new(&address), + Cell::new(&path), + Cell::new(&name), + ]); + } + + Ok(table) +} + +pub async fn retrieve_address_string<T: AddressOrVaultName>( data: &Data, - mnemonic: &str, - password: String, -) -> Result<AccountId, GcliError> { - // check validity by deriving keypair - let keypair = pair_from_str(mnemonic)?; - let address = keypair.public(); - // write encrypted mnemonic in file identified by pubkey - let path = data.project_dir.data_dir().join(address.to_string()); - let mut file = std::fs::File::create(path)?; - file.write_all(&encrypt(mnemonic.as_bytes(), password).map_err(|e| anyhow!(e))?[..])?; - Ok(keypair.public().into()) -} - -/// try get secret in keystore -pub fn try_fetch_secret(data: &Data, address: AccountId) -> Result<Option<String>, GcliError> { - let path = data.project_dir.data_dir().join(address.to_string()); + address_or_vault_name: T, +) -> Result<String, GcliError> { + if let Some(address) = address_or_vault_name.address() { + return Ok(address.to_string()); + } + + let derivation = retrieve_vault_derivation(data, address_or_vault_name).await?; + + Ok(derivation.address) +} + +pub async fn retrieve_vault_derivation<T: AddressOrVaultName>( + data: &Data, + address_or_vault_name: T, +) -> Result<vault_derivation::Model, GcliError> { + let derivation = if let Some(name) = address_or_vault_name.name() { + let (name, derivation_path_opt) = + parse_prefix_and_derivation_path_from_string(name.to_string())?; + + let derivation = vault_derivation::Entity::find() + .filter(vault_derivation::Column::Name.eq(Some(name.clone()))) + .one(data.connection.as_ref().unwrap()) + .await?; + + let derivation = derivation.ok_or(GcliError::Input(format!( + "No vault derivation found with name:'{name}'" + )))?; + + match derivation_path_opt { + None => derivation, + Some(path) => { + let sub_derivation = vault_derivation::Entity::find() + .filter( + vault_derivation::Column::RootAddress.eq(derivation.root_address.clone()), + ) + .filter(vault_derivation::Column::Path.eq(Some(path.clone()))) + .one(data.connection.as_ref().unwrap()) + .await?; + + sub_derivation.ok_or(GcliError::Input(format!( + "No vault derivation found with root name:'{name}' and path:'{path}'" + )))? + } + } + } else if let Some(address) = address_or_vault_name.address() { + let derivation = vault_derivation::Entity::find_by_id(address.to_string()) + .one(data.connection.as_ref().unwrap()) + .await?; + + derivation.ok_or(GcliError::Input(format!( + "No vault derivation found with Address:'{address}'" + )))? + } else { + //Should never happen since clap enforces exactly one of the 2 options + return Err(GcliError::Input("No address or name provided".to_string())); + }; + Ok(derivation) +} + +fn create_vault_data_to_import<F, P>( + secret_format: SecretFormat, + prompt_fn: F, +) -> Result<VaultDataToImport, GcliError> +where + F: Fn() -> (keys::Secret, P), + P: Into<KeyPair>, +{ + let (secret, pair) = prompt_fn(); + let key_pair = pair.into(); + Ok(VaultDataToImport { + secret_format, + secret, + address: key_pair.address(), + key_pair, + }) +} + +fn prompt_secret_and_compute_vault_data_to_import( + secret_format: SecretFormat, +) -> Result<VaultDataToImport, GcliError> { + match secret_format { + SecretFormat::Substrate => { + create_vault_data_to_import(secret_format, prompt_secret_substrate_and_compute_keypair) + } + SecretFormat::Seed => { + create_vault_data_to_import(secret_format, prompt_seed_and_compute_keypair) + } + SecretFormat::Cesium => { + create_vault_data_to_import(secret_format, prompt_secret_cesium_and_compute_keypair) + } + SecretFormat::Predefined => { + create_vault_data_to_import(secret_format, prompt_predefined_and_compute_keypair) + } + } +} + +/// Creates derivation and if necessary root vault account and root derivation +/// +/// Does it all using "db" parameter that should better be a transaction since multiple operations can be done +pub async fn create_derivation_for_vault_data_to_import<C>( + db: &C, + vault_data: &VaultDataToImport, + password: &str, + name: Option<&String>, +) -> Result<vault_derivation::Model, GcliError> +where + C: ConnectionTrait, +{ + //To be safe + if vault_derivation::Entity::find_by_id(vault_data.address.to_string()) + .one(db) + .await? + .is_some() + { + return Err(GcliError::Input(format!( + "Vault entry already exists for address {}", + vault_data.address + ))); + } + + let secret_suri: String = match &vault_data.secret_format { + SecretFormat::Cesium => { + if let KeyPair::Nacl(keypair) = &vault_data.key_pair { + // In case of cesium key, we will store the seed suri instead of id/password (so it supports derivations) + let seed: [u8; 32] = keypair.skey[0..32] + .try_into() + .expect("slice with incorrect length"); + format!("0x{}", hex::encode(seed)) + } else { + return Err(GcliError::Input("Expected KeyPair::Nacl".to_string())); + } + } + SecretFormat::Seed => { + if let keys::Secret::SimpleSecret(seed_str) = &vault_data.secret { + format!("0x{seed_str}") + } else { + return Err(GcliError::Input("Expected SimpleSecret".to_string())); + } + } + SecretFormat::Substrate | SecretFormat::Predefined => { + if let keys::Secret::SimpleSecret(secret_suri) = &vault_data.secret { + secret_suri.clone() + } else { + return Err(GcliError::Input("Expected SimpleSecret".to_string())); + } + } + }; + + let secret_format = vault_data.secret_format; + + let (root_secret_suri, derivation_path_opt, root_address, derivation_address) = + compute_root_and_derivation_data(&secret_format, secret_suri)?; + + // Making sure the computed address is the same as the address to import + let address_to_import = vault_data.address.to_string(); + if let Some(derivation_address) = &derivation_address { + if *derivation_address != address_to_import { + return Err(GcliError::Input(format!( + "Derivation address {} does not match the expected address {}", + derivation_address, address_to_import + ))); + } + } else if root_address != address_to_import { + return Err(GcliError::Input(format!( + "Derivation address {} does not match the expected address {}", + root_address, address_to_import + ))); + } + + let encrypted_private_key = + encrypt(root_secret_suri.as_bytes(), password.to_string()).map_err(|e| anyhow!(e))?; + + let _root_account = vault_account::create_vault_account( + db, + &root_address, + map_secret_format_to_crypto_type(secret_format), + encrypted_private_key, + ) + .await?; + + let derivation = if let Some(derivation_path) = derivation_path_opt { + // Root derivation + let _root_derivation = + vault_derivation::create_root_vault_derivation(db, &root_address, None).await?; + + // Compute derivation ! + let derivation = vault_derivation::ActiveModel { + address: Set(derivation_address.unwrap().clone()), + name: Set(name.cloned()), + path: Set(Some(derivation_path)), + root_address: Set(root_address.clone()), + }; + let derivation = derivation.insert(db).await?; + println!("Created derivation {}", derivation); + derivation + } else { + let derivation = + vault_derivation::create_root_vault_derivation(db, &root_address, name).await?; + println!("Created derivation {}", derivation); + derivation + }; + + Ok(derivation) +} + +fn compute_root_and_derivation_data( + secret_format: &SecretFormat, + secret_suri: String, +) -> Result<(String, Option<String>, String, Option<String>), GcliError> { + let (root_secret_suri, derivation_path_opt) = + parse_prefix_and_derivation_path_from_string(secret_suri)?; + + let (root_address, derivation_address_opt) = match &secret_format { + SecretFormat::Cesium => match &derivation_path_opt { + None => { + let root_suri = &root_secret_suri; + let root_pair = pair_from_ed25519_str(root_suri)?; + let root_address: AccountId = root_pair.public().into(); + + (root_address.to_string(), None) + } + Some(derivation_path) => { + let root_suri = &root_secret_suri; + let root_pair = pair_from_ed25519_str(root_suri)?; + let root_address: AccountId = root_pair.public().into(); + + let derivation_suri = root_suri.clone() + derivation_path; + let derivation_pair = pair_from_ed25519_str(&derivation_suri)?; + let derivation_address: AccountId = derivation_pair.public().into(); + + ( + root_address.to_string(), + Some(derivation_address.to_string()), + ) + } + }, + SecretFormat::Substrate | SecretFormat::Seed | SecretFormat::Predefined => { + match &derivation_path_opt { + None => { + let root_suri = &root_secret_suri; + let root_pair = pair_from_sr25519_str(root_suri)?; + let root_address: AccountId = root_pair.public().into(); + + (root_address.to_string(), None) + } + Some(derivation_path) => { + let root_suri = &root_secret_suri; + let root_pair = pair_from_sr25519_str(root_suri)?; + let root_address: AccountId = root_pair.public().into(); + + let derivation_suri = root_suri.clone() + derivation_path; + let derivation_pair = pair_from_sr25519_str(&derivation_suri)?; + let derivation_address: AccountId = derivation_pair.public().into(); + + ( + root_address.to_string(), + Some(derivation_address.to_string()), + ) + } + } + } + }; + Ok(( + root_secret_suri, + derivation_path_opt, + root_address, + derivation_address_opt, + )) +} + +fn get_vault_key_path(data: &Data, vault_filename: &str) -> PathBuf { + data.project_dir.data_dir().join(vault_filename) +} + +/// look for different possible paths for vault keys and return both format and path +fn find_substrate_vault_key_file(data: &Data, address: &str) -> Result<Option<PathBuf>, GcliError> { + let path = get_vault_key_path(data, address); if path.exists() { + return Ok(Some(path)); + } + + Ok(None) +} + +/// try to get secret in keystore, prompt for the password and compute the keypair +pub async fn try_fetch_key_pair( + data: &Data, + address: AccountId, +) -> Result<Option<KeyPair>, GcliError> { + if let Some(derivation) = vault_derivation::Entity::find_by_id(address.to_string()) + .one(data.connection.as_ref().unwrap()) + .await? + { + if let Some(vault_account) = + vault_account::Entity::find_by_id(derivation.root_address.clone()) + .one(data.connection.as_ref().unwrap()) + .await? + { + let root_secret_suri = retrieve_suri_from_vault_account(&vault_account)?; + + let secret_suri = if let Some(derivation_path) = derivation.path { + format!("{root_secret_suri}{derivation_path}") + } else { + root_secret_suri + }; + + let key_pair = compute_keypair(vault_account.crypto_type, &secret_suri)?; + + //To be safe + if address != key_pair.address() { + return Err(GcliError::Input(format!( + "Computed address {} does not match the expected address {}", + key_pair.address(), + address + ))); + } + + Ok(Some(key_pair)) + } else { + Ok(None) + } + } else { + Ok(None) + } +} + +pub fn retrieve_suri_from_vault_account( + vault_account: &vault_account::Model, +) -> Result<String, GcliError> { + let password = inputs::prompt_password()?; + + let cypher = &vault_account.encrypted_private_key; + let secret_vec = + decrypt(cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?; + let secret_suri = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?; + + Ok(secret_suri) +} + +pub fn compute_keypair(crypto_type: CryptoType, secret_suri: &str) -> Result<KeyPair, GcliError> { + let key_pair = match crypto_type { + CryptoType::Sr25519Mnemonic | CryptoType::Sr25519Seed => { + pair_from_sr25519_str(secret_suri)?.into() + } + CryptoType::Ed25519Seed => pair_from_ed25519_str(secret_suri)?.into(), + }; + Ok(key_pair) +} + +pub struct VaultDataFromFile { + address: String, + secret_format: SecretFormat, + secret: String, + path: PathBuf, + password: String, + key_pair: KeyPair, +} + +/// try to get secret in keystore, prompt for the password and compute the keypair +pub fn try_fetch_vault_data_from_file( + data: &Data, + address: &str, +) -> Result<Option<VaultDataFromFile>, GcliError> { + if let Some(path) = find_substrate_vault_key_file(data, address)? { println!("Enter password to unlock account {address}"); let password = rpassword::prompt_password("Password: ")?; - let mut file = std::fs::OpenOptions::new().read(true).open(path)?; + let mut file = std::fs::OpenOptions::new().read(true).open(path.clone())?; let mut cypher = vec![]; file.read_to_end(&mut cypher)?; - let secret = decrypt(&cypher, password).map_err(|e| GcliError::Input(e.to_string()))?; - let secretstr = String::from_utf8(secret).map_err(|e| anyhow!(e))?; - Ok(Some(secretstr)) + let secret_vec = + decrypt(&cypher, password.clone()).map_err(|e| GcliError::Input(e.to_string()))?; + let secret = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?; + + let key_pair = pair_from_sr25519_str(&secret)?.into(); + + Ok(Some(VaultDataFromFile { + address: address.to_string(), + secret_format: SecretFormat::Substrate, + secret, + path, + password, + key_pair, + })) } else { Ok(None) } } -// test that armored encryption/decryption work as intended -#[test] -fn test_encrypt_decrypt() { - let plaintext = b"Hello world!"; - let passphrase = "this is not a good passphrase".to_string(); - let encrypted = encrypt(plaintext, passphrase.clone()).unwrap(); - let decrypted = decrypt(&encrypted, passphrase).unwrap(); - assert_eq!(decrypted, plaintext); +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + /// test that armored encryption/decryption work as intended + #[test] + fn test_encrypt_decrypt() { + let plaintext = b"Hello world!"; + let passphrase = "this is not a good passphrase".to_string(); + let encrypted = encrypt(plaintext, passphrase.clone()).unwrap(); + let decrypted = decrypt(&encrypted, passphrase).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[rstest] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk//0"), + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + Some(String::from("//0")) + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + Some(String::from("//0")) + )] + #[case( + String::from( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice" + ), + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + Some(String::from("//Alice")) + )] + #[case( + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + String::from("bottom drive obey lake curtain smoke basket hold race lonely fit walk"), + None + )] + #[case( + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + String::from("0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + None + )] + #[case( + String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + String::from("fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"), + None + )] + #[case( + String::from("someVaultName//Alice"), + String::from("someVaultName"), + Some(String::from("//Alice")) + )] + #[case(String::from("someVaultName"), String::from("someVaultName"), None)] + fn test_parse_prefix_and_derivation_path_from_string( + #[case] raw_string: String, + #[case] expected_prefix: String, + #[case] expected_derivation_path: Option<String>, + ) { + let (root_secret, derivation_path) = + parse_prefix_and_derivation_path_from_string(raw_string).unwrap(); + assert_eq!(expected_prefix, root_secret); + assert_eq!(expected_derivation_path, derivation_path); + } } diff --git a/src/conf.rs b/src/conf.rs index 31a13ef..a6f6ff7 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,3 +1,4 @@ +use crate::entities::vault_derivation; use crate::*; use serde::{Deserialize, Serialize}; @@ -72,7 +73,7 @@ pub enum Subcommand { } /// handle conf command -pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { +pub async fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> { // match subcommand match command { Subcommand::Where => { @@ -83,10 +84,19 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> } Subcommand::Show => { println!("{}", data.cfg); + if let Some(account_id) = data.cfg.address { + if let Some(derivation) = vault_derivation::fetch_vault_derivation( + data.connection.as_ref().unwrap(), + account_id.to_string().as_str(), + ) + .await? + { + println!("(Vault entry: {})", derivation); + } + } } Subcommand::Save => { - confy::store(APP_NAME, None, &data.cfg).expect("unable to write config"); - println!("Configuration updated!"); + save_config(&data); } Subcommand::Default => { confy::store(APP_NAME, None, Config::default()).expect("unable to write config"); @@ -95,3 +105,8 @@ pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> Ok(()) } + +pub fn save_config(data: &Data) { + confy::store(APP_NAME, None, &data.cfg).expect("unable to write config"); + println!("Configuration updated!"); +} diff --git a/src/data.rs b/src/data.rs index 5dbc79d..5061c5d 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,11 +1,13 @@ -use std::str::FromStr; - +use crate::commands::vault; use crate::*; use indexer::Indexer; +use sea_orm::DatabaseConnection; +use std::str::FromStr; // consts pub const LOCAL_DUNITER_ENDPOINT: &str = "ws://localhost:9944"; pub const LOCAL_INDEXER_ENDPOINT: &str = "http://localhost:4350/graphql"; +pub const SQLITE_DB_FILENAME: &str = "gcli.sqlite"; const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(feature = "gdev")] @@ -28,27 +30,29 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [ /// Data of current command /// can also include fetched information pub struct Data { - // command line arguments + /// command line arguments pub args: Args, - // config + /// config pub cfg: conf::Config, - // rpc to substrate client + /// database connection + pub connection: Option<DatabaseConnection>, + /// rpc to substrate client pub client: Option<Client>, - // graphql to duniter-indexer + /// graphql to duniter-indexer pub indexer: Option<Indexer>, - // user keypair + /// user keypair pub keypair: Option<KeyPair>, - // user identity index + /// user identity index pub idty_index: Option<IdtyId>, - // token decimals + /// token decimals pub token_decimals: u32, - // token symbol + /// token symbol pub token_symbol: String, - // genesis hash + /// genesis hash pub genesis_hash: Hash, - // indexer genesis hash + /// indexer genesis hash pub indexer_genesis_hash: Hash, - // gcli base path + /// gcli base path pub project_dir: directories::ProjectDirs, } @@ -70,6 +74,7 @@ impl Default for Data { project_dir, args: Default::default(), cfg: Default::default(), + connection: Default::default(), client: Default::default(), indexer: Default::default(), keypair: Default::default(), @@ -85,15 +90,18 @@ impl Default for Data { // implement helper functions for Data impl Data { /// --- constructor --- - pub fn new(args: Args) -> Result<Self, GcliError> { - Self { + pub async fn new(args: Args) -> Result<Self, GcliError> { + let mut data = Self { args, cfg: conf::load_conf(), token_decimals: 0, token_symbol: "tokens".into(), ..Default::default() - } - .overwrite_from_args() + }; + //Necessary to support checking "vault names" in the base arguments + data = data.build_connection().await?; + data = data.overwrite_from_args().await?; + Ok(data) } // --- getters --- // the "unwrap" should not fail if data is well prepared @@ -106,11 +114,11 @@ impl Data { pub fn address(&self) -> AccountId { self.cfg.address.clone().expect("an address is needed") } - pub fn keypair(&self) -> KeyPair { + pub async fn keypair(&self) -> KeyPair { match self.keypair.clone() { Some(keypair) => keypair, None => loop { - match fetch_or_get_keypair(self, self.cfg.address.clone()) { + match fetch_or_get_keypair(self, self.cfg.address.clone()).await { Ok(pair) => return pair, Err(e) => println!("{e:?} → retry"), } @@ -136,7 +144,7 @@ impl Data { } // --- mutators --- /// use arguments to overwrite config - pub fn overwrite_from_args(mut self) -> Result<Self, GcliError> { + pub async fn overwrite_from_args(mut self) -> Result<Self, GcliError> { // network if let Some(network) = self.args.network.clone() { // a network was provided as arugment @@ -184,8 +192,10 @@ impl Data { self.cfg.address = Some(keypair.address()); self.keypair = Some(keypair); } - // address - if let Some(address) = self.args.address.clone() { + // address or vault name + if let Some(address_or_vault_name) = self.args.address_or_vault_name.clone() { + let address = vault::retrieve_address_string(&self, address_or_vault_name).await?; + self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); // if giving address, cancel secret self.keypair = None @@ -224,7 +234,16 @@ impl Data { }; Ok(self) } - /// get issuer index + + /// build a database connection + pub async fn build_connection(mut self) -> Result<Self, GcliError> { + let data_dir = self.project_dir.data_dir(); + let connection = database::build_sqlite_connection(data_dir, SQLITE_DB_FILENAME).await?; + self.connection = Some(connection); + Ok(self) + } + + /// get issuer index<br> /// needs address and client first pub async fn fetch_idty_index(mut self) -> Result<Self, GcliError> { self.idty_index = Some( diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..fad4f71 --- /dev/null +++ b/src/database.rs @@ -0,0 +1,71 @@ +use crate::entities::{vault_account, vault_derivation}; +use crate::utils::GcliError; +use sea_orm::sea_query::IndexCreateStatement; +use sea_orm::{ConnectionTrait, Database, DatabaseConnection, Schema}; +use std::fs; +use std::path::Path; + +pub async fn build_sqlite_connection( + data_dir: &Path, + filename: &str, +) -> Result<DatabaseConnection, GcliError> { + let sqlite_path = data_dir.join(filename); + + // Check if the file exists, and create it if it doesn't (otherwise the connection will fail) + if !Path::new(&sqlite_path).exists() { + fs::File::create(sqlite_path.clone())?; + } + + let sqlite_path_str = sqlite_path + .into_os_string() + .into_string() + .map_err(|_| GcliError::Input("Invalid SQLite path".to_string()))?; + + let sqlite_db_url = format!("sqlite://{}", sqlite_path_str); + + let connection = initialize_db(&sqlite_db_url).await?; + Ok(connection) +} + +pub async fn initialize_db(db_url: &str) -> Result<DatabaseConnection, GcliError> { + let db = Database::connect(db_url).await?; + let schema = Schema::new(db.get_database_backend()); + + create_table_if_not_exists(&db, &schema, vault_account::Entity).await?; + create_table_if_not_exists(&db, &schema, vault_derivation::Entity).await?; + + Ok(db) +} + +async fn create_table_if_not_exists<E: sea_orm::EntityTrait>( + db: &DatabaseConnection, + schema: &Schema, + entity: E, +) -> Result<(), GcliError> { + db.execute( + db.get_database_backend() + .build(schema.create_table_from_entity(entity).if_not_exists()), + ) + .await?; + Ok(()) +} + +/// The only way to add composed unique index... +#[allow(dead_code)] +async fn create_table_if_not_exists_with_index<E: sea_orm::EntityTrait>( + db: &DatabaseConnection, + schema: &Schema, + entity: E, + index: &mut IndexCreateStatement, +) -> Result<(), GcliError> { + db.execute( + db.get_database_backend().build( + schema + .create_table_from_entity(entity) + .index(index) + .if_not_exists(), + ), + ) + .await?; + Ok(()) +} diff --git a/src/entities.rs b/src/entities.rs new file mode 100644 index 0000000..2de801c --- /dev/null +++ b/src/entities.rs @@ -0,0 +1,2 @@ +pub mod vault_account; +pub mod vault_derivation; diff --git a/src/entities/vault_account.rs b/src/entities/vault_account.rs new file mode 100644 index 0000000..649d0b8 --- /dev/null +++ b/src/entities/vault_account.rs @@ -0,0 +1,106 @@ +use crate::inputs; +use crate::utils::GcliError; +use sea_orm::prelude::StringLen; +use sea_orm::ActiveValue::Set; +use sea_orm::{ + ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef, + RelationTrait, +}; +use sea_orm::{ActiveModelTrait, ConnectionTrait, PrimaryKeyTrait}; +use sea_orm::{DeriveActiveEnum, EntityTrait}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "vault_account")] +pub struct Model { + /// SS58 Address of (root) account + #[sea_orm(primary_key, auto_increment = false)] + pub address: String, + pub crypto_type: CryptoType, + pub encrypted_private_key: Vec<u8>, +} + +impl Display for Model { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[address:\"{}\"]", self.address) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)] +#[sea_orm( + rs_type = "String", + db_type = "String(StringLen::None)", + rename_all = "PascalCase" +)] +pub enum CryptoType { + /// The secret key or BIP39 mnemonic + Sr25519Mnemonic, + /// The 32B SR25519 seed without "0x" prefix + Sr25519Seed, + /// The 32B ED25519 seed without "0x" prefix (for cesium) + Ed25519Seed, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Derivation, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Derivation => Entity::has_many(super::vault_derivation::Entity).into(), + } + } +} + +// `Related` trait has to be implemented by hand +impl Related<super::vault_derivation::Entity> for Entity { + fn to() -> RelationDef { + Relation::Derivation.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +/// Creates a (root) vault account (if it doesn't exist) and returns it +pub async fn create_vault_account<C>( + db: &C, + address: &str, + crypto_type: CryptoType, + encrypted_private_key: Vec<u8>, +) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + let vault_account = Entity::find_by_id(address.to_owned()).one(db).await?; + + Ok(match vault_account { + Some(vault_account) => { + println!("Already existing (root) vault account {vault_account}"); + + let overwrite_key = + inputs::confirm_action("Do you want to overwrite with the new encrypted key ?")?; + if overwrite_key { + let mut vault_account: ActiveModel = vault_account.into(); + vault_account.encrypted_private_key = Set(encrypted_private_key); + let vault_account = vault_account.update(db).await?; + println!("Updated (root) vault account {vault_account}"); + + vault_account + } else { + vault_account + } + } + None => { + let vault_account = ActiveModel { + address: Set(address.to_owned()), + crypto_type: Set(crypto_type), + encrypted_private_key: Set(encrypted_private_key), + }; + let vault_account = vault_account.insert(db).await?; + println!("Created vault account {}", vault_account); + vault_account + } + }) +} diff --git a/src/entities/vault_derivation.rs b/src/entities/vault_derivation.rs new file mode 100644 index 0000000..16beab6 --- /dev/null +++ b/src/entities/vault_derivation.rs @@ -0,0 +1,150 @@ +use crate::utils::GcliError; +use sea_orm::sea_query::NullOrdering; +use sea_orm::ActiveValue::Set; +use sea_orm::PrimaryKeyTrait; +use sea_orm::QueryFilter; +use sea_orm::{ + ActiveModelBehavior, DeriveEntityModel, DerivePrimaryKey, EnumIter, Related, RelationDef, + RelationTrait, +}; +use sea_orm::{ActiveModelTrait, ColumnTrait}; +use sea_orm::{ConnectionTrait, EntityTrait, Order, QueryOrder}; +use std::fmt::Display; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "vault_derivation")] +pub struct Model { + /// SS58 Address + #[sea_orm(primary_key, auto_increment = false)] + pub address: String, + /// Optional name for the derivation + #[sea_orm(unique)] + pub name: Option<String>, + /// derivation path - None if for the root account + pub path: Option<String>, + /// ForeignKey to root vault_account SS58 Address + pub root_address: String, +} + +impl Display for Model { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "[address:\"{}\", name:{:?}, path:{:?}, root_address:\"{}\"]", + self.address, self.name, self.path, self.root_address + ) + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + RootAccount, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::RootAccount => Entity::belongs_to(super::vault_account::Entity) + .from(Column::RootAddress) + .to(super::vault_account::Column::Address) + .into(), + } + } +} + +// `Related` trait has to be implemented by hand +impl Related<super::vault_account::Entity> for Entity { + fn to() -> RelationDef { + Relation::RootAccount.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +/// Creates a root derivation (if it doesn't exist) and returns it +pub async fn create_root_vault_derivation<C>( + db: &C, + root_address: &str, + root_name: Option<&String>, +) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + create_vault_derivation(db, root_address, root_address, root_name, None).await +} + +/// Creates a derivation (if it doesn't exist) and returns it +pub async fn create_vault_derivation<C>( + db: &C, + address: &str, + root_address: &str, + name: Option<&String>, + path: Option<&String>, +) -> Result<Model, GcliError> +where + C: ConnectionTrait, +{ + let derivation = Entity::find_by_id(root_address.to_owned()).one(db).await?; + + Ok(match derivation { + Some(derivation) => derivation, + None => { + let derivation = ActiveModel { + address: Set(address.to_owned()), + name: Set(name.cloned()), + path: Set(path.cloned()), + root_address: Set(root_address.to_owned()), + }; + derivation.insert(db).await? + } + }) +} + +pub async fn fetch_vault_derivation<C>(db: &C, address: &str) -> Result<Option<Model>, GcliError> +where + C: ConnectionTrait, +{ + let derivation = Entity::find_by_id(address.to_owned()).one(db).await?; + Ok(derivation) +} + +pub async fn fetch_all_linked_derivations_in_order<C>( + db: &C, + root_address: &str, +) -> Result<Vec<Model>, GcliError> +where + C: ConnectionTrait, +{ + let linked_derivations = Entity::find() + .filter(Column::RootAddress.eq(root_address.to_owned())) + .order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First) + .all(db) + .await?; + Ok(linked_derivations) +} + +pub async fn list_all_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError> +where + C: ConnectionTrait, +{ + let derivations = Entity::find() + .order_by_asc(Column::RootAddress) + .order_by_with_nulls(Column::Path, Order::Asc, NullOrdering::First) + .all(db) + .await?; + + Ok(derivations) +} + +pub async fn list_all_root_derivations_in_order<C>(db: &C) -> Result<Vec<Model>, GcliError> +where + C: ConnectionTrait, +{ + let derivations = Entity::find() + .filter(Column::Path.is_null()) + .order_by_asc(Column::RootAddress) + .all(db) + .await?; + + Ok(derivations) +} diff --git a/src/inputs.rs b/src/inputs.rs new file mode 100644 index 0000000..1eb188e --- /dev/null +++ b/src/inputs.rs @@ -0,0 +1,85 @@ +use crate::utils::GcliError; + +pub fn prompt_password() -> Result<String, GcliError> { + prompt_password_query("Password") +} + +pub fn prompt_password_confirm() -> Result<String, GcliError> { + prompt_password_query_confirm("Password") +} + +pub fn prompt_password_query(query: impl ToString) -> Result<String, GcliError> { + dialoguer::Password::default() + .with_prompt(query.to_string()) + .allow_empty_password(true) + .interact() + .map_err(|e| GcliError::Input(e.to_string())) +} + +pub fn prompt_password_query_confirm(query: impl ToString) -> Result<String, GcliError> { + dialoguer::Password::new() + .with_prompt(query.to_string()) + .allow_empty_password(true) + .with_confirmation( + format!("Repeat {}", query.to_string()), + "Error: the values do not match.", + ) + .interact() + .map_err(|e| GcliError::Input(e.to_string())) +} + +/// Prompt for a (direct) vault name (cannot contain derivation path) +/// +/// Also preventing to use '<' and '>' as those are used in the display of +pub fn prompt_vault_name() -> Result<Option<String>, GcliError> { + let name = dialoguer::Input::new() + .with_prompt("Name") + .validate_with({ + move |input: &String| -> Result<(), &str> { + if input.contains('<') || input.contains('>') || input.contains('/') { + Err("Name cannot contain characters '<', '>', '/'") + } else { + Ok(()) + } + } + }) + .allow_empty(true) + .interact_text() + .map_err(|e| GcliError::Input(e.to_string()))?; + + let name = if name.trim().is_empty() { + None + } else { + Some(name.trim().to_string()) + }; + + Ok(name) +} + +/// Prompt for a derivation path +pub fn prompt_vault_derivation_path() -> Result<String, GcliError> { + let path = dialoguer::Input::new() + .with_prompt("Derivation path") + .validate_with({ + move |input: &String| -> Result<(), &str> { + if !input.starts_with("/") { + Err("derivation path needs to start with one or more '/'") + } else { + Ok(()) + } + } + }) + .allow_empty(false) + .interact_text() + .map_err(|e| GcliError::Input(e.to_string()))?; + + Ok(path) +} + +pub fn confirm_action(query: impl ToString) -> Result<bool, GcliError> { + dialoguer::Confirm::new() + .with_prompt(query.to_string()) + //.default(false) + .interact() + .map_err(|e| GcliError::Input(e.to_string())) +} diff --git a/src/keys.rs b/src/keys.rs index ae695c9..2a687cc 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,5 +1,7 @@ use crate::*; -use sr25519::Pair as Sr25519Pair; +use sp_core::ed25519; +use sp_core::ed25519::Pair as Ed25519Pair; +use sp_core::sr25519::Pair as Sr25519Pair; pub const SUBSTRATE_MNEMONIC: &str = "bottom drive obey lake curtain smoke basket hold race lonely fit walk"; @@ -17,6 +19,12 @@ pub enum SecretFormat { /// Cesium (scrypt + nacl) Cesium, } + +pub enum Secret { + SimpleSecret(String), + DualSecret(String, String), +} + impl FromStr for SecretFormat { type Err = std::io::Error; @@ -47,14 +55,21 @@ impl From<SecretFormat> for OsStr { } /// wrapper type for keys + signature +//FIXME check if it's ok to keep large enum variant +// Sr25519 second-largest variant contains at least 256 bytes +// Ed25519 largest variant contains at least 480 bytes +#[allow(clippy::large_enum_variant)] pub enum KeyPair { Sr25519(Sr25519Pair), + Ed25519(Ed25519Pair), + //FIXME Cleanup Nacl(nacl::sign::Keypair), } impl KeyPair { pub fn address(&self) -> AccountId { match self { KeyPair::Sr25519(keypair) => keypair.public().into(), + KeyPair::Ed25519(keypair) => keypair.public().into(), KeyPair::Nacl(keypair) => keypair.pkey.into(), } } @@ -64,6 +79,7 @@ impl Clone for KeyPair { fn clone(&self) -> Self { match self { KeyPair::Sr25519(keypair) => KeyPair::Sr25519(keypair.clone()), + KeyPair::Ed25519(keypair) => KeyPair::Ed25519(*keypair), KeyPair::Nacl(keypair) => KeyPair::Nacl(nacl::sign::Keypair { skey: keypair.skey, pkey: keypair.pkey, @@ -76,6 +92,11 @@ impl From<Sr25519Pair> for KeyPair { KeyPair::Sr25519(pair) } } +impl From<Ed25519Pair> for KeyPair { + fn from(pair: Ed25519Pair) -> KeyPair { + KeyPair::Ed25519(pair) + } +} impl From<nacl::sign::Keypair> for KeyPair { fn from(pair: nacl::sign::Keypair) -> KeyPair { KeyPair::Nacl(pair) @@ -83,6 +104,8 @@ impl From<nacl::sign::Keypair> for KeyPair { } pub enum Signature { Sr25519(sr25519::Signature), + Ed25519(ed25519::Signature), + //FIXME Cleanup Nacl(Vec<u8>), } @@ -106,9 +129,9 @@ pub fn pair_from_secret( secret: &str, ) -> Result<Sr25519Pair, GcliError> { match secret_format { - SecretFormat::Substrate => pair_from_str(secret), - SecretFormat::Predefined => pair_from_str(secret), /* if predefined, secret arg is replaced in config */ - SecretFormat::Seed => pair_from_seed(secret), + SecretFormat::Substrate => pair_from_sr25519_str(secret), + SecretFormat::Predefined => pair_from_sr25519_str(secret), /* if predefined, secret arg is replaced in config */ + SecretFormat::Seed => pair_from_sr25519_seed(secret), SecretFormat::Cesium => Err(GcliError::Logic( "cesium format incompatible with single secret".to_string(), )), @@ -116,7 +139,7 @@ pub fn pair_from_secret( } /// get keypair from given string secret -pub fn pair_from_str(secret: &str) -> Result<Sr25519Pair, GcliError> { +pub fn pair_from_sr25519_str(secret: &str) -> Result<Sr25519Pair, GcliError> { Sr25519Pair::from_string(secret, None) .map_err(|_| GcliError::Input("Invalid secret".to_string())) } @@ -124,7 +147,7 @@ pub fn pair_from_str(secret: &str) -> Result<Sr25519Pair, GcliError> { /// get keypair from given seed // note: Sr25519Pair::from_string does exactly that when seed is 0x prefixed // (see from_string_with_seed method in crypto core) -pub fn pair_from_seed(secret: &str) -> Result<Sr25519Pair, GcliError> { +pub fn pair_from_sr25519_seed(secret: &str) -> Result<Sr25519Pair, GcliError> { let mut seed = [0; 32]; hex::decode_to_slice(secret, &mut seed) .map_err(|_| GcliError::Input("Invalid secret".to_string()))?; @@ -132,6 +155,22 @@ pub fn pair_from_seed(secret: &str) -> Result<Sr25519Pair, GcliError> { Ok(pair) } +/// get keypair from given ed25519 string secret (used for cesium) +pub fn pair_from_ed25519_str(secret: &str) -> Result<Ed25519Pair, GcliError> { + Ed25519Pair::from_string(secret, None) + .map_err(|_| GcliError::Input("Invalid secret".to_string())) +} + +/// get keypair from given ed25519 seed (used for cesium) +#[allow(unused)] +pub fn pair_from_ed25519_seed(secret: &str) -> Result<Ed25519Pair, GcliError> { + let mut seed = [0; 32]; + hex::decode_to_slice(secret, &mut seed) + .map_err(|_| GcliError::Input("Invalid secret".to_string()))?; + let pair = Ed25519Pair::from_seed(&seed); + Ok(pair) +} + /// get mnemonic from predefined derivation path pub fn predefined_mnemonic(deriv: &str) -> String { format!("{SUBSTRATE_MNEMONIC}//{deriv}") @@ -139,7 +178,7 @@ pub fn predefined_mnemonic(deriv: &str) -> String { /// get keypair from predefined secret pub fn pair_from_predefined(deriv: &str) -> Result<Sr25519Pair, GcliError> { - pair_from_str(&predefined_mnemonic(deriv)) + pair_from_sr25519_str(&predefined_mnemonic(deriv)) } /// get keypair from Cesium id/pwd @@ -152,10 +191,15 @@ pub fn pair_from_cesium(id: String, pwd: String) -> nacl::sign::Keypair { /// ask user to input a secret pub fn prompt_secret_substrate() -> Sr25519Pair { + // Only interested in the keypair which is the second element of the tuple + prompt_secret_substrate_and_compute_keypair().1 +} + +pub fn prompt_secret_substrate_and_compute_keypair() -> (Secret, Sr25519Pair) { loop { - let mnemonic = &rpassword::prompt_password("Mnemonic: ").unwrap(); - match pair_from_str(mnemonic) { - Ok(pair) => return pair, + let mnemonic = rpassword::prompt_password("Mnemonic: ").unwrap(); + match pair_from_sr25519_str(&mnemonic) { + Ok(pair) => return (Secret::SimpleSecret(mnemonic), pair), Err(_) => println!("Invalid secret"), } } @@ -163,17 +207,30 @@ pub fn prompt_secret_substrate() -> Sr25519Pair { /// ask user pass (Cesium format) pub fn prompt_secret_cesium() -> nacl::sign::Keypair { + // Only interested in the keypair which is the second element of the tuple + prompt_secret_cesium_and_compute_keypair().1 +} + +pub fn prompt_secret_cesium_and_compute_keypair() -> (Secret, nacl::sign::Keypair) { let id = rpassword::prompt_password("Cesium id: ").unwrap(); let pwd = rpassword::prompt_password("Cesium password: ").unwrap(); - pair_from_cesium(id, pwd) + ( + Secret::DualSecret(id.clone(), pwd.clone()), + pair_from_cesium(id, pwd), + ) } /// ask user to input a seed pub fn prompt_seed() -> Sr25519Pair { + // Only interested in the keypair which is the second element of the tuple + prompt_seed_and_compute_keypair().1 +} + +pub fn prompt_seed_and_compute_keypair() -> (Secret, Sr25519Pair) { loop { - let seed = &rpassword::prompt_password("Seed: ").unwrap(); - match pair_from_seed(seed) { - Ok(pair) => return pair, + let seed = rpassword::prompt_password("Seed: ").unwrap(); + match pair_from_sr25519_seed(&seed) { + Ok(pair) => return (Secret::SimpleSecret(seed), pair), Err(_) => println!("Invalid seed"), } } @@ -181,8 +238,16 @@ pub fn prompt_seed() -> Sr25519Pair { /// ask user pass (Cesium format) pub fn prompt_predefined() -> Sr25519Pair { + // Only interested in the keypair which is the second element of the tuple + prompt_predefined_and_compute_keypair().1 +} + +pub fn prompt_predefined_and_compute_keypair() -> (Secret, Sr25519Pair) { let deriv = rpassword::prompt_password("Enter derivation path: ").unwrap(); - pair_from_predefined(&deriv).expect("invalid secret") + ( + Secret::SimpleSecret(predefined_mnemonic(&deriv)), + pair_from_predefined(&deriv).expect("invalid secret"), + ) } /// ask user secret in relevant format @@ -196,7 +261,10 @@ pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair { } /// get the secret from user, trying first keystore then input -pub fn fetch_or_get_keypair(data: &Data, address: Option<AccountId>) -> Result<KeyPair, GcliError> { +pub async fn fetch_or_get_keypair( + data: &Data, + address: Option<AccountId>, +) -> Result<KeyPair, GcliError> { if let Some(address) = address { // if address corresponds to predefined, (for example saved to config) // keypair is already known (useful for dev mode) @@ -204,9 +272,9 @@ pub fn fetch_or_get_keypair(data: &Data, address: Option<AccountId>) -> Result<K return Ok(pair_from_predefined(d).unwrap().into()); }; - // look for corresponding secret in keystore - if let Some(secret) = commands::vault::try_fetch_secret(data, address)? { - return get_keypair(SecretFormat::Substrate, Some(&secret)); + // look for corresponding KeyPair in keystore + if let Some(key_pair) = commands::vault::try_fetch_key_pair(data, address).await? { + return Ok(key_pair); }; } // at the moment, there is no way to confg gcli to use an other kind of secret @@ -225,3 +293,358 @@ fn catch_known(address: &str) -> Option<&str> { _ => None, } } + +// Unit tests +#[cfg(test)] +mod tests { + use super::*; + mod substrate { + use super::*; + + /// Testing sr25519 mnemonic derivations + /// + /// Using `subkey` command to have expected values from mnemonic derivations (using `SUBSTRATE_MNEMONIC` for tests) + /// + /// ##### The root mnemonic + /// ``` + /// subkey inspect + /// URI: + /// Secret phrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk + /// Network ID: substrate + /// Secret seed: 0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e + /// Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Public key (SS58): 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// SS58 Address: 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// ``` + /// + /// ##### The '//0' derivation + /// ``` + /// subkey inspect + /// URI: + /// Secret Key URI `bottom drive obey lake curtain smoke basket hold race lonely fit walk//0` is account: + /// Network ID: substrate + /// Secret seed: 0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393 + /// Public key (hex): 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972 + /// Account ID: 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972 + /// Public key (SS58): 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH + /// SS58 Address: 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH + /// ``` + #[test] + fn test_sr25519_mnemonic_derivations() { + let root_sr25519_pair = pair_from_sr25519_str(SUBSTRATE_MNEMONIC).unwrap(); + + let expected_root_ss58_address_string = + "5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV".to_string(); + let root_ss58_address: AccountId = root_sr25519_pair.public().into(); + println!("root SS58 Address: '{}'", root_ss58_address); + assert_eq!( + expected_root_ss58_address_string, + root_ss58_address.to_string() + ); + + // Using derive on root keypair to get '//0' + let (deriv_0_sr25519_pair, _seed) = root_sr25519_pair + .derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None) + .unwrap(); + + let expected_deriv_0_ss58_address_string = + "5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH".to_string(); + let deriv_0_ss58_address: AccountId = deriv_0_sr25519_pair.public().into(); + println!("derived '//0' SS58 Address: '{}'", deriv_0_ss58_address); + assert_eq!( + expected_deriv_0_ss58_address_string, + deriv_0_ss58_address.to_string() + ); + + // Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri + let deriv_0_suri = SUBSTRATE_MNEMONIC.to_string() + "//0"; + let deriv_0_suri_sr25519_pair = + sp_core::sr25519::Pair::from_string(&deriv_0_suri, None).unwrap(); + let deriv_0_suri_ss58_address: AccountId = deriv_0_suri_sr25519_pair.public().into(); + println!( + "derived '//0' from suri SS58 Address: '{}'", + deriv_0_suri_ss58_address + ); + assert_eq!( + expected_deriv_0_ss58_address_string, + deriv_0_suri_ss58_address.to_string() + ); + } + } + + mod seed { + use super::*; + + /// Testing sr25519 seed derivations + /// + /// Using `subkey` command to have expected values from seed derivations (using seed linked to `SUBSTRATE_MNEMONIC` for tests) + /// + /// ##### The root seed + /// ``` + /// subkey inspect + /// URI: + /// Secret Key URI `0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e` is account: + /// Network ID: substrate + /// Secret seed: 0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e + /// Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Public key (SS58): 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// SS58 Address: 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// ``` + /// + /// ##### The '//0' derivation + /// ``` + /// subkey inspect + /// URI: + /// Secret Key URI `0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e//0` is account: + /// Network ID: substrate + /// Secret seed: 0x914dded06277afbe5b0e8a30bce539ec8a9552a784d08e530dc7c2915c478393 + /// Public key (hex): 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972 + /// Account ID: 0x2afba9278e30ccf6a6ceb3a8b6e336b70068f045c666f2e7f4f9cc5f47db8972 + /// Public key (SS58): 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH + /// SS58 Address: 5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH + /// ``` + #[test] + fn test_sr25519_seed_derivations() { + let root_seed = "fac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e"; + let root_sr25519_pair = pair_from_sr25519_seed(root_seed).unwrap(); + + let expected_root_ss58_address_string = + "5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV".to_string(); + let root_ss58_address: AccountId = root_sr25519_pair.public().into(); + println!("root SS58 Address: '{}'", root_ss58_address); + assert_eq!( + expected_root_ss58_address_string, + root_ss58_address.to_string() + ); + + // Using derive on root keypair to get "//0" + let (deriv_0_sr25519_pair, _seed) = root_sr25519_pair + .derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None) + .unwrap(); + + let expected_deriv_0_ss58_address_string = + "5D34dL5prEUaGNQtPPZ3yN5Y6BnkfXunKXXz6fo7ZJbLwRRH".to_string(); + let deriv_0_ss58_address: AccountId = deriv_0_sr25519_pair.public().into(); + println!("derived '//0' SS58 Address: '{}'", deriv_0_ss58_address); + assert_eq!( + expected_deriv_0_ss58_address_string, + deriv_0_ss58_address.to_string() + ); + + // Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri + let deriv_0_suri = "0x".to_string() + root_seed + "//0"; + let deriv_0_suri_sr25519_pair = + sp_core::sr25519::Pair::from_string(&deriv_0_suri, None).unwrap(); + let deriv_0_suri_ss58_address: AccountId = deriv_0_suri_sr25519_pair.public().into(); + println!( + "derived '//0' from suri SS58 Address: '{}'", + deriv_0_suri_ss58_address + ); + assert_eq!( + expected_deriv_0_ss58_address_string, + deriv_0_suri_ss58_address.to_string() + ); + } + } + + mod predefined { + use super::*; + + /// Testing predefined mnemonic derivations (using names instead of indexes) + /// + /// Using `subkey` command to have expected values from predefined mnemonic derivations + /// + /// ##### The root mnemonic + /// ``` + /// subkey inspect + /// URI: + /// Secret phrase: bottom drive obey lake curtain smoke basket hold race lonely fit walk + /// Network ID: substrate + /// Secret seed: 0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e + /// Public key (hex): 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Account ID: 0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a + /// Public key (SS58): 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// SS58 Address: 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV + /// ``` + /// + /// ##### The '//Alice' derivation + /// ``` + /// subkey inspect + /// URI: + /// Secret Key URI `bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice` is account: + /// Network ID: substrate + /// Secret seed: 0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a + /// Public key (hex): 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d + /// Account ID: 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d + /// Public key (SS58): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY + /// SS58 Address: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY + /// ``` + #[test] + fn test_predefined_mnemonic_derivations() { + let root_sr25519_pair = pair_from_sr25519_str(SUBSTRATE_MNEMONIC).unwrap(); + + let expected_root_ss58_address_string = + "5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV".to_string(); + let root_ss58_address: AccountId = root_sr25519_pair.public().into(); + println!("root SS58 Address: '{}'", root_ss58_address); + assert_eq!( + expected_root_ss58_address_string, + root_ss58_address.to_string() + ); + + // Using derive on root keypair to get Alice + let (alice_sr25519_pair, _seed) = root_sr25519_pair + .derive( + Some(sp_core::DeriveJunction::hard("Alice")).into_iter(), + None, + ) + .unwrap(); + + let expected_alice_ss58_address_string = + "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY".to_string(); + let alice_ss58_address: AccountId = alice_sr25519_pair.public().into(); + println!("Alice SS58 Address: '{}'", alice_ss58_address); + assert_eq!( + expected_alice_ss58_address_string, + alice_ss58_address.to_string() + ); + + // Using sp_core::sr25519::Pair::from_string(suri, None) to derive keypair from suri + let alice_suri = SUBSTRATE_MNEMONIC.to_owned() + "//Alice"; + let alice_suri_sr25519_pair = + sp_core::sr25519::Pair::from_string(&alice_suri, None).unwrap(); + let alice_suri_ss58_address: AccountId = alice_suri_sr25519_pair.public().into(); + println!("Alice suri SS58 Address: '{}'", alice_suri_ss58_address); + assert_eq!( + expected_alice_ss58_address_string, + alice_suri_ss58_address.to_string() + ); + } + } + + mod cesium { + use super::*; + + /// Test which verifies that it's possible to derive a key coming from a cesium v1 id & password + /// + /// Using subkey command with **ed25519** scheme to show we can derive a key from a seed + /// and to retrieve expected values. + /// + /// ##### Without derivation (using seed from the test) + /// ``` + /// subkey inspect --scheme ed25519 + /// URI: + /// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84` is account: + /// Network ID: substrate + /// Secret seed: 0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84 + /// Public key (hex): 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74 + /// Account ID: 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74 + /// Public key (SS58): 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 + /// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 + /// ``` + /// + /// ##### With derivation '//0' (using seed from the test) + /// ``` + /// subkey inspect --scheme ed25519 + /// URI: + /// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0` is account: + /// Network ID: substrate + /// Secret seed: 0x916e95359a49c82e3d84269b90551433352c433eb9bb270fb8cb86e8a6c9ec85 + /// Public key (hex): 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de + /// Account ID: 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de + /// Public key (SS58): 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf + /// SS58 Address: 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf + /// ``` + #[test] + fn test_cesium_v1_key_derivation() { + let cesium_id = "test_cesium_id".to_string(); + let cesium_pwd = "test_cesium_pwd".to_string(); + + let cesium_keypair = pair_from_cesium(cesium_id, cesium_pwd); + + println!( + "Nacl keypair: pkey:'0x{}' skey:'0x{}'", + hex::encode(cesium_keypair.pkey), + hex::encode(cesium_keypair.skey) + ); + + let cesium_v1_pubkey = bs58::encode(cesium_keypair.pkey).into_string(); + println!("Cesium v1 Pubkey: '{}'", cesium_v1_pubkey); + let cesium_v1_address: AccountId = cesium_keypair.pkey.into(); + println!("SS58 Address: '{}'", cesium_v1_address); + + //ed25519 seed **seems** to be the first 32 bytes of the secret key from nacl keypair + let mut seed: [u8; 32] = [0; 32]; + seed.copy_from_slice(&cesium_keypair.skey[0..32]); + println!(); + println!("ed25519 seed: '0x{}'", hex::encode(seed)); + + let ed25519_pair_from_seed = sp_core::ed25519::Pair::from_seed(&seed); + + println!(); + println!( + "ed25519 keypair from seed : public:'0x{}' raw_vec:'0x{}'", + hex::encode(ed25519_pair_from_seed.public().0), + hex::encode(ed25519_pair_from_seed.to_raw_vec().as_slice()) + ); + let ed25519_address_from_seed: AccountId = ed25519_pair_from_seed.public().into(); + println!( + "ed25519 keypair from seed : public SS58 Address:'{}'", + ed25519_address_from_seed + ); + assert_eq!(cesium_v1_address, ed25519_address_from_seed); + + let root_suri = "0x".to_string() + &hex::encode(seed); + let ed25519_pair_from_suri = + sp_core::ed25519::Pair::from_string(&root_suri, None).unwrap(); + let ed25519_address_from_suri: AccountId = ed25519_pair_from_suri.public().into(); + println!( + "ed25519 keypair from suri : public SS58 Address:'{}'", + ed25519_address_from_suri + ); + assert_eq!(cesium_v1_address, ed25519_address_from_suri); + + // Tested derivation manually with `subkey` command using adapted suri: `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0` + let expected_ss58_address_derivation_0 = + "5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf".to_string(); + + // Using derive on the ed25519 keypair + let (derived_ed25519_pair, _seed) = ed25519_pair_from_seed + .derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None) + .unwrap(); + println!(); + println!( + "derived ed25519 keypair: public:'0x{}' raw_vec:'0x{}'", + hex::encode(derived_ed25519_pair.public().0), + hex::encode(derived_ed25519_pair.to_raw_vec().as_slice()) + ); + let derived_ed25519_address = + subxt::utils::AccountId32::from(derived_ed25519_pair.public()); + println!( + "derived ed25519 keypair: public SS58 Address:'{}'", + derived_ed25519_address + ); + assert_eq!( + expected_ss58_address_derivation_0, + derived_ed25519_address.to_string() + ); + + // Using sp_core::ed25519::Pair::from_string(suri, None) to derive keypair from suri + let deriv_0_suri = root_suri.clone() + "//0"; + let derived_ed25519_pair_from_suri = + sp_core::ed25519::Pair::from_string(&deriv_0_suri, None).unwrap(); + let derived_ed25519_address_from_suri: AccountId = + derived_ed25519_pair_from_suri.public().into(); + println!( + "derived ed25519 keypair from suri : public SS58 Address:'{}'", + derived_ed25519_address_from_suri + ); + assert_eq!( + expected_ss58_address_derivation_0, + derived_ed25519_address_from_suri.to_string() + ); + } + } +} diff --git a/src/main.rs b/src/main.rs index ba4857a..68b37cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ mod commands; mod conf; mod data; +mod database; mod display; +mod entities; mod indexer; +mod inputs; mod keys; mod runtime_config; mod utils; @@ -51,8 +54,8 @@ pub struct Args { #[clap(short = 'S', long)] secret_format: Option<SecretFormat>, /// Address - #[clap(short, long)] - address: Option<String>, + #[clap(flatten)] + address_or_vault_name: Option<AddressOrVaultNameGroupOptional>, /// Overwrite duniter websocket RPC endpoint #[clap(short, long)] url: Option<String>, @@ -67,6 +70,51 @@ pub struct Args { output_format: OutputFormat, } +trait AddressOrVaultName { + fn address(&self) -> Option<&AccountId>; + fn name(&self) -> Option<&String>; +} + +#[derive(Debug, clap::Args, Clone)] +#[group(required = false, multiple = false)] +pub struct AddressOrVaultNameGroupOptional { + /// Account Address + #[clap(short)] + address: Option<AccountId>, + /// Vault name for that account + #[clap(short = 'v')] + name: Option<String>, +} + +impl AddressOrVaultName for AddressOrVaultNameGroupOptional { + fn address(&self) -> Option<&AccountId> { + self.address.as_ref() + } + fn name(&self) -> Option<&String> { + self.name.as_ref() + } +} + +#[derive(Debug, clap::Args, Clone)] +#[group(required = true, multiple = false)] +pub struct AddressOrVaultNameGroup { + /// Account Address + #[clap(short)] + address: Option<AccountId>, + /// Vault name for that account + #[clap(short = 'v')] + name: Option<String>, +} + +impl AddressOrVaultName for AddressOrVaultNameGroup { + fn address(&self) -> Option<&AccountId> { + self.address.as_ref() + } + fn name(&self) -> Option<&String> { + self.name.as_ref() + } +} + // TODO derive the fromstr implementation /// secret format #[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] @@ -158,7 +206,7 @@ async fn main() -> Result<(), GcliError> { env_logger::init(); // parse argument and initialize data - let data = Data::new(Args::parse())?; + let data = Data::new(Args::parse()).await?; // match subcommands let result = match data.args.subcommand.clone() { @@ -182,8 +230,8 @@ async fn main() -> Result<(), GcliError> { commands::blockchain::handle_command(data, subcommand).await } Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, - Subcommand::Config(subcommand) => conf::handle_command(data, subcommand), - Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand), + Subcommand::Config(subcommand) => conf::handle_command(data, subcommand).await, + Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand).await, Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await, Subcommand::Publish => commands::publish::handle_command().await, }; diff --git a/src/utils.rs b/src/utils.rs index 05d7252..2393bd3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use crate::*; +use sea_orm::DbErr; /// track progress of transaction on the network /// until it is in block with success or failure @@ -56,14 +57,19 @@ pub async fn submit_call<TxPayload: Payload>( .await?; // sign and submit - match data.keypair() { + match data.keypair().await { // sr25519 key pair KeyPair::Sr25519(keypair) => data.client().tx().create_signed_offline( payload, &PairSigner::<Runtime, sp_core::sr25519::Pair>::new(keypair), DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(), ), - // nacl key pair + KeyPair::Ed25519(keypair) => data.client().tx().create_signed_offline( + payload, + &PairSigner::<Runtime, sp_core::ed25519::Pair>::new(keypair), + DefaultExtrinsicParamsBuilder::new().nonce(nonce).build(), + ), + //FIXME cleanup KeyPair::Nacl(keypair) => data.client().tx().create_signed_offline( payload, &commands::cesium::CesiumSigner::new(keypair), @@ -93,19 +99,29 @@ pub fn look_event<E: std::fmt::Debug + StaticEvent + DisplayEvent>( /// custom error type intended to provide more convenient error message to user #[derive(Debug)] pub enum GcliError { + //TODO Check allowing dead code is ok for those (we are using the values only in case of exception) /// error coming from subxt Subxt(subxt::Error), /// error coming from duniter + #[allow(dead_code)] Duniter(String), /// error coming from indexer + #[allow(dead_code)] Indexer(String), + /// error coming from database + #[allow(dead_code)] + DatabaseError(DbErr), /// logic error (illegal operation or security) + #[allow(dead_code)] Logic(String), /// input error + #[allow(dead_code)] Input(String), /// error coming from anyhow (to be removed) + #[allow(dead_code)] Anyhow(anyhow::Error), /// error coming from io + #[allow(dead_code)] IoError(std::io::Error), } impl std::fmt::Display for GcliError { @@ -141,3 +157,8 @@ impl From<std::io::Error> for GcliError { GcliError::IoError(error) } } +impl From<sea_orm::DbErr> for GcliError { + fn from(error: sea_orm::DbErr) -> Self { + GcliError::DatabaseError(error) + } +} -- GitLab