diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..60337f19f07141c1b7e5278c3b64a9dd43775a3f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing + +For any addition of feature or modification of existing feature, please discuss it beforehand via an issue of this repository by tagging one or more maintainers. + +## Commit Message Guidelines + +We have very precise rules over how our git commit messages can be formatted. This leads to **more +readable messages** that are easy to follow when looking through the **project history**. + +### Commit Message Format + +Each commit message consists of a **header**, a **body** and a **footer**. The header has a special +format that includes a **type**, a **scope** and a **subject**: + +```txt +<type>(<scope>): <subject> +<BLANK LINE> +<body> +<BLANK LINE> +<footer> +``` + +The **header** is mandatory and the **scope** of the header is optional. + +Any line of the commit message cannot be longer 100 characters! This allows the message to be easier +to read on GitHub as well as in various git tools. + +The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any. + +```txt +docs(changelog): update changelog to beta.5 +``` + +```txt +fix(release): need to depend on latest rxjs and zone.js + +The version in our package.json gets copied to the one we publish, and users need the latest of these. +``` + +### Revert + +If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit being reverted. + +### Type + +Must be one of the following: + +* **build**: Changes that affect the build system or external dependencies (example scopes: crypto, wot) +* **chore**: Modification of the repository architecture +* **ci**: Changes to our CI configuration files and scripts (example scopes: Github Actions, Gitlab CI) +* **docs**: Documentation only changes +* **feat**: Add a new feature +* **mod**: Modify an existing feature +* **fix**: A bug fix +* **perf**: A code change that improves performance +* **refactor**: A code change that neither fixes a bug nor adds a feature nor modify an existing feature +* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +* **test**: Adding missing tests or correcting existing tests + +### Subject + +The subject contains a succinct description of the change: + +* use the imperative, present tense: "change" not "changed" nor "changes" +* don't capitalize the first letter +* no dot (.) at the end + +### Body + +Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". +The body should include the motivation for the change and contrast this with previous behavior. + +### Footer + +The footer should contain any information about **Breaking Changes** and is also the place to +reference issues that this commit **Closes**. + +**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000000000000000000000000000000000000..35711874710eb6b50ec22bccb37e9646beceb6cd --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2795 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +dependencies = [ + "serde", +] + +[[package]] +name = "async-bincode" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31c08aa335b3ab414d29bdefe1f4353408abf93f3db1e3e2cc78d3ec4f0d43" +dependencies = [ + "bincode", + "byteorder", + "bytes 1.0.1", + "futures-core", + "futures-sink", + "serde", + "tokio", +] + +[[package]] +name = "async-graphql" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5e71af65ee4a367603829e92b26710caf4116d1eaca8953dedf1809b33694a" +dependencies = [ + "async-graphql-derive", + "async-graphql-parser", + "async-graphql-value", + "async-stream", + "async-trait", + "fnv", + "futures-util", + "http", + "indexmap", + "log", + "multer", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "static_assertions", + "tempfile", + "thiserror", +] + +[[package]] +name = "async-graphql-derive" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd4c2eb837e894909fe13509f2351fa3990c114426e41255936800892ccbe26" +dependencies = [ + "Inflector", + "async-graphql-parser", + "darling", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "thiserror", +] + +[[package]] +name = "async-graphql-parser" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8d8116f3015b7686ef98ffb70a74183c3c17bf45135993d3f095812e09e786" +dependencies = [ + "async-graphql-value", + "pest", + "pest_derive", + "serde", + "serde_json", +] + +[[package]] +name = "async-graphql-value" +version = "2.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8342ada84efe4b3d59e1313d1d2740a8ccfc76ddb57ccf55e45a6464dd7d0d3" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-oneshot" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f4770cbbff928c30a991de67fb3976f44d8e3e202f8c79ef91b47006e04904" +dependencies = [ + "futures-micro", +] + +[[package]] +name = "async-rwlock" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261803dcc39ba9e72760ba6e16d0199b1eef9fc44e81bffabbebb9f5aea3906c" +dependencies = [ + "async-mutex", + "event-listener", +] + +[[package]] +name = "async-stream" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a26cb53174ddd320edfff199a853f93d571f48eeb4dde75e67a9a3dbb7b7e5e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db134ba52475c060f3329a8ef0f8786d6b872ed01515d4b79c162e5798da1340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_stream" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d5ad740b7193a31e80950ab7fece57c38d426fcd23a729d9d7f4cf15bb63f94" +dependencies = [ + "futures", + "rustc_version", + "tokio", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "beef" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6736e2428df2ca2848d846c43e88745121a6654696e349ce0054a420815a7409" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake3" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9ff35b701f3914bdb8fad3368d822c766ef2858b2583198e41639b936f09d3f" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if 0.1.10", + "constant_time_eq", + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "cmake" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" +dependencies = [ + "cc", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.4", + "subtle", +] + +[[package]] +name = "cryptoxide" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46212f5d1792f89c3e866fb10636139464060110c568edd7f73ab5e9f736c26d" + +[[package]] +name = "ctor" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b1b72f1263f214c0f823371768776c4f5841b942c9883aa8e5ec584fd0ba6" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.4", +] + +[[package]] +name = "downcast" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb454f0228b18c7f4c3b0ebbee346ed9c52e7443b0999cd543ff3571205701d" + +[[package]] +name = "dubp" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914faa8052c72c8b2f513a44398123379d70a59dfedf0aa8dc7b581ee223fbfc" +dependencies = [ + "dubp-block", + "dubp-common", + "dubp-documents", + "dubp-documents-parser", + "dubp-wallet", + "dup-crypto", +] + +[[package]] +name = "dubp-block" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15cc90473a86c4987ea34211829d491dfb56f7c09ba79ac3d57d9430782d038" +dependencies = [ + "dubp-documents", + "dubp-documents-parser", + "json-pest-parser", + "log", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "dubp-common" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a5a6cc11940e0a85f492325fec45c557c5f103c92ea445427b4272c1a12395" +dependencies = [ + "dup-crypto", + "serde", + "serde_json", + "thiserror", + "zerocopy", +] + +[[package]] +name = "dubp-documents" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85d43233426a5a24a5d22e98da2d8f0efab9739a58af15fa27e74a213b2d5bb9" +dependencies = [ + "beef", + "dubp-wallet", + "log", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "dubp-documents-parser" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac382364d99af3c235530f9de41a1833d18a16dff8833a7b351e8946d378de18" +dependencies = [ + "dubp-documents", + "json-pest-parser", + "pest", + "pest_derive", + "serde_json", + "thiserror", +] + +[[package]] +name = "dubp-wallet" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47cc059e6b139def809f9d0bf776a21f5c2d59fefc20ed30c7aceedfef8de703" +dependencies = [ + "byteorder", + "dubp-common", + "serde", + "smallvec", + "thiserror", + "zerocopy", +] + +[[package]] +name = "duniter-bc-reader" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "anyhow", + "dubp", + "duniter-dbs", + "resiter", +] + +[[package]] +name = "duniter-bca" +version = "0.1.0" +dependencies = [ + "anyhow", + "arrayvec", + "async-bincode", + "async_io_stream", + "bincode", + "dubp", + "duniter-bca-types", + "duniter-core", + "duniter-gva-db", + "duniter-gva-dbs-reader", + "fast-threadpool", + "futures", + "mockall", + "once_cell", + "smallvec", + "tokio", + "uninit", +] + +[[package]] +name = "duniter-bca-types" +version = "0.1.0" +dependencies = [ + "arrayvec", + "bincode", + "dubp", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "duniter-conf" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "dubp", + "serde", +] + +[[package]] +name = "duniter-core" +version = "1.8.1" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "duniter-bc-reader", + "duniter-conf", + "duniter-dbs", + "duniter-dbs-write-ops", + "duniter-global", + "duniter-mempools", + "duniter-module", +] + +[[package]] +name = "duniter-dbs" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "arrayvec", + "bincode", + "byteorder", + "chrono", + "dubp", + "kv_typed", + "log", + "parking_lot", + "paste", + "rand 0.7.3", + "serde", + "serde_json", + "smallvec", + "thiserror", + "uninit", + "zerocopy", +] + +[[package]] +name = "duniter-dbs-write-ops" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "chrono", + "dubp", + "duniter-dbs", + "duniter-global", + "fast-threadpool", + "flume", + "log", + "resiter", +] + +[[package]] +name = "duniter-global" +version = "1.8.1" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "async-rwlock", + "dubp", + "duniter-dbs", + "flume", + "mockall", + "once_cell", + "tokio", +] + +[[package]] +name = "duniter-gva" +version = "0.1.0" +dependencies = [ + "anyhow", + "arrayvec", + "async-graphql", + "async-mutex", + "async-trait", + "bytes 1.0.1", + "dubp", + "duniter-bca", + "duniter-core", + "duniter-gva-db", + "duniter-gva-dbs-reader", + "duniter-gva-gql", + "duniter-gva-indexer", + "fast-threadpool", + "flume", + "futures", + "http", + "log", + "mockall", + "resiter", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "unwrap", + "warp", +] + +[[package]] +name = "duniter-gva-db" +version = "0.1.0" +dependencies = [ + "bincode", + "chrono", + "dubp", + "duniter-core", + "kv_typed", + "parking_lot", + "paste", + "serde", + "serde_json", + "uninit", + "zerocopy", +] + +[[package]] +name = "duniter-gva-dbs-reader" +version = "0.1.0" +dependencies = [ + "anyhow", + "arrayvec", + "dubp", + "duniter-bca-types", + "duniter-core", + "duniter-gva-db", + "maplit", + "mockall", + "resiter", + "smallvec", + "unwrap", +] + +[[package]] +name = "duniter-gva-gql" +version = "0.1.0" +dependencies = [ + "anyhow", + "arrayvec", + "async-graphql", + "async-trait", + "dubp", + "duniter-core", + "duniter-gva-db", + "duniter-gva-dbs-reader", + "fast-threadpool", + "flume", + "futures", + "log", + "mockall", + "pretty_assertions", + "resiter", + "serde", + "serde_json", + "tokio", + "unwrap", +] + +[[package]] +name = "duniter-gva-indexer" +version = "0.1.0" +dependencies = [ + "anyhow", + "dubp", + "duniter-core", + "duniter-gva-db", + "maplit", + "once_cell", + "resiter", + "smallvec", +] + +[[package]] +name = "duniter-mempools" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "dubp", + "duniter-bc-reader", + "duniter-dbs", + "duniter-dbs-write-ops", + "log", + "thiserror", +] + +[[package]] +name = "duniter-module" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "anyhow", + "async-trait", + "dubp", + "duniter-conf", + "duniter-dbs", + "duniter-global", + "duniter-mempools", + "fast-threadpool", + "log", +] + +[[package]] +name = "dup-crypto" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b8d3e1c65e3ed89db6973e807e9c355c8f9078866402e695a16683f1e226d2" +dependencies = [ + "aes", + "arrayvec", + "base64", + "blake3", + "bs58", + "byteorder", + "cryptoxide", + "ed25519-bip32", + "getrandom 0.2.2", + "once_cell", + "ring", + "serde", + "thiserror", + "zerocopy", + "zeroize", +] + +[[package]] +name = "ed25519-bip32" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8827180a2b511141fbe49141e50b31a8d542465e0fb572f81f36feea2addfe92" +dependencies = [ + "cryptoxide", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fast-threadpool" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0585e8f3a04d8c4a65927a5cb5e42c6ce641528b4fc294af9d7990fcd6c4b86a" +dependencies = [ + "async-oneshot", + "flume", + "num_cpus", +] + +[[package]] +name = "float-cmp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "flume" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fce69af4d4582ea989e6adfc5c9b81fd2071ff89234e5c14675c82a85217df" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spinning_top", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a039c3498dc930fe810151a34ba0c1c70b02b8625035592e74432f678591f2" + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815" + +[[package]] +name = "futures-executor" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f6cb7042eda00f0049b1d2080aa4b93442997ee507eb3828e8bd7577f94c9d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04" + +[[package]] +name = "futures-macro" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668c6733a182cd7deb4f1de7ba3bf2120823835b3bcfbeacf7d2c4a773c1bb8b" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-micro" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9325be55c5581082cd110294fa988c1f920bc573ec370ef201e33c469a95a" + +[[package]] +name = "futures-sink" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23" + +[[package]] +name = "futures-task" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc" + +[[package]] +name = "futures-util" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "h2" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc018e188373e2777d0ef2467ebff62a08e66c3f5857b23c8fbec3018210dc00" +dependencies = [ + "bytes 1.0.1", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "headers" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64", + "bitflags", + "bytes 1.0.1", + "headers-core", + "http", + "mime", + "sha-1 0.9.4", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes 1.0.1", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfb77c123b4e2f72a2069aeae0b4b4949cc7e966df277813fc16347e7549737" +dependencies = [ + "bytes 1.0.1", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1ce40d6fc9764887c2fdc7305c3dcc429ba11ff981c1509416afd5697e4437" + +[[package]] +name = "httpdate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" + +[[package]] +name = "hyper" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f006b8784cfb01fe7aa9c46f5f5cd4cf5c85a8c612a0653ec97642979062665" +dependencies = [ + "bytes 1.0.1", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes 1.0.1", +] + +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "jobserver" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-pest-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc5c84a2bceeda1ce3bd58497bde2d8cba61ca0b45873ef502401f0ff2ae8ed" +dependencies = [ + "pest", + "pest_derive", + "thiserror", + "unwrap", +] + +[[package]] +name = "kv_typed" +version = "0.1.0" +source = "git+https://git.duniter.org/nodes/rust/duniter-core#2b71d3e8f0bd223f84a010e500e2d47a99c0c4eb" +dependencies = [ + "byteorder", + "cfg-if 0.1.10", + "flume", + "leveldb_minimal", + "parking_lot", + "paste", + "rayon", + "regex", + "serde_json", + "sled", + "smallvec", + "thiserror", + "uninit", + "zerocopy", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "leveldb-sys" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618aee5ba3d32cb8456420a9a454aa71c1af5b3e9c7a2ec20a0f3cbbe47246cb" +dependencies = [ + "cmake", + "libc", + "num_cpus", +] + +[[package]] +name = "leveldb_minimal" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b4cb22d7d3cce486fc6e2ef7cd25d38e118f525f79c4a946ac48d89c5d16b1" +dependencies = [ + "leveldb-sys", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" + +[[package]] +name = "lock_api" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "memoffset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "mockall" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d614ad23f9bb59119b8b5670a85c7ba92c5e9adf4385c81ea00c51c8be33d5" +dependencies = [ + "cfg-if 1.0.0", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd4234635bca06fc96c7368d038061e0aae1b00a764dc817e900dc974e3deea" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multer" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99851e6ad01b0fbe086dda2dea00d68bb84fc7d7eae2c39ca7313da9197f4d31" +dependencies = [ + "bytes 0.5.6", + "derive_more", + "encoding_rs", + "futures", + "http", + "httparse", + "lazy_static", + "log", + "mime", + "regex", + "twoway 0.2.1", +] + +[[package]] +name = "multipart" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.7.3", + "safemem", + "tempfile", + "twoway 0.1.8", +] + +[[package]] +name = "nanorand" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1378b66f7c93a1c0f8464a19bf47df8795083842e5090f4b7305973d5a22d0" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "paste" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1 0.8.2", +] + +[[package]] +name = "pin-project" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + +[[package]] +name = "predicates" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb433456c1a57cc93554dea3ce40b4c19c4057e41c55d4a0f3d84ea71c325aa" +dependencies = [ + "difference", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" + +[[package]] +name = "predicates-tree" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f553275e5721409451eb85e15fd9a860a6e5ab4496eb215987502b5f5391f2" +dependencies = [ + "predicates-core", + "treeline", +] + +[[package]] +name = "pretty_assertions" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" +dependencies = [ + "ansi_term", + "ctor", + "diff", + "output_vt100", +] + +[[package]] +name = "proc-macro-crate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +dependencies = [ + "libc", + "rand_chacha 0.3.0", + "rand_core 0.6.2", + "rand_hc 0.3.0", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.2", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +dependencies = [ + "getrandom 0.2.2", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +dependencies = [ + "rand_core 0.6.2", +] + +[[package]] +name = "rayon" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "resiter" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69ab1e90258b7769f0b5c46bfd802b8206d0707ced4ca4b9d5681b744de1be" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha-1" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if 1.0.0", + "cpuid-bool", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "sled" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0132f3e393bcb7390c60bb45769498cf4550bcb7a21d7f95c02b69f6362cdc" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot", + "zstd", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spinning_top" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd0ab6b8c375d2d963503b90d3770010d95bc3b5f98036f948dee24bf4e8879" +dependencies = [ + "lock_api", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" + +[[package]] +name = "syn" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9505f307c872bab8eb46f77ae357c8eba1fdacead58ee5a850116b1d7f82883" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "rand 0.8.3", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5" +dependencies = [ + "autocfg", + "bytes 1.0.1", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940a12c99365c31ea8dd9ba04ec1be183ffe4920102bb7122c2f515437601e8e" +dependencies = [ + "bytes 1.0.1", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +dependencies = [ + "cfg-if 1.0.0", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64", + "byteorder", + "bytes 1.0.1", + "http", + "httparse", + "input_buffer", + "log", + "rand 0.8.3", + "sha-1 0.9.4", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "twoway" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +dependencies = [ + "memchr", + "unchecked-index", +] + +[[package]] +name = "typenum" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" +dependencies = [ + "matches", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "uninit" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce382f462302087c8effe69a6c9e84ae8ce6a9cc541d921d0bb5d1fd789cdbf" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "unwrap" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e33648dd74328e622c7be51f3b40a303c63f93e6fa5f08778b6203a4c25c20f" + +[[package]] +name = "url" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +dependencies = [ + "bytes 1.0.1", + "futures", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" + +[[package]] +name = "web-sys" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zerocopy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6580539ad917b7c026220c4b3f2c08d52ce54d6ce0dc491e66002e35388fab46" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9c39e6d503229ffa00cc2954af4a751e6bbedf2a2c18e856eb3ece93d32495" +dependencies = [ + "proc-macro2", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zstd" +version = "0.5.4+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69996ebdb1ba8b1517f61387a883857818a66c8a295f487b1ffd8fd9d2c82910" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "2.0.6+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98aa931fb69ecee256d44589d19754e61851ae4769bf963b385119b1cc37a49e" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.4.18+zstd.1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6e8778706838f43f771d80d37787cb2fe06dafe89dd3aebaf6721b9eaec81" +dependencies = [ + "cc", + "glob", + "itertools", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..dc661b030a5b69c7acfe0a9eab80c3c32059308c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "duniter-gva" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +license = "AGPL-3.0" +edition = "2018" + +[dependencies] +anyhow = "1.0.33" +arrayvec = "0.5.1" +async-graphql = { version = "2.8", features = ["log"] } +async-mutex = "1.4.0" +async-trait = "0.1.41" +bytes = "1.0" +dubp = { version = "0.51.0", features = ["duniter"] } +duniter-bca = { path = "./bca" } +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +duniter-gva-db = { path = "./db" } +duniter-gva-dbs-reader = { path = "./dbs-reader" } +duniter-gva-indexer = { path = "./indexer" } +duniter-gva-gql = { path = "./gql" } +fast-threadpool = "0.2.3" +flume = "0.10.0" +futures = "0.3.6" +http = "0.2.1" +log = "0.4.11" +resiter = "0.4.0" +serde = { version = "1.0.105", features = ["derive"] } +serde_urlencoded = "0.7.0" +tokio = { version = "1.2", features = ["io-util", "rt-multi-thread"] } +warp = "0.3" + +[dev-dependencies] +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core", features = ["mem"] } +mockall = "0.9.1" +serde_json = "1.0.53" +tokio = { version = "1.2", features = ["macros", "rt-multi-thread", "time"] } +unwrap = "1.2.1" + +[workspace] +members = [ + "bca", + "db", + "dbs-reader", + "gql", + "indexer", +] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5902c3a1f074a8b132fadc72d489341c6692701f --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Duniter GVA + +This repository contains the code of the GVA module. + +## Duniter repositories + +Duniter's code is separated into several git repositories: + +* **[dubp-rs-libs]** contains the logic common to Duniter and its customers. +* **[duniter-core]** contains the core code of Duniter. +* The gitlab subgroup **[nodes/rust/modules]** contains the main Duniter modules code (gva, admin, etc). +* The **[duniter]** subgroup contains the "official" implementations of the "duniter-cli" and "duniter-desktop" programs with their default modules (also contains the historical implementation being migrated). + +[DuniterModule]: https://git.duniter.org/nodes/rust/duniter-core/blob/main/module/src/lib.rs#L41 +[duniter gitlab]: https://git.duniter.org +[dubp-rs-libs]: https://git.duniter.org/libs/dubp-rs-libs +[duniter-core]: https://git.duniter.org/nodes/rust/duniter-core +[duniter]: https://git.duniter.org/nodes/typescript/duniter +[nodes/rust/modules]: https://git.duniter.org/nodes/rust/modules diff --git a/bca/Cargo.toml b/bca/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d29a25dd9212ca469bc015ed5e185c1d6560fbaf --- /dev/null +++ b/bca/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "duniter-bca" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +license = "AGPL-3.0" +edition = "2018" + +[dependencies] +anyhow = "1.0.33" +arrayvec = { version = "0.5.1", features = ["serde"] } +async-bincode = "0.6.1" +async_io_stream = { version = "0.3.1", features = [ "tokio_io"] } +bincode = "1.3" +dubp = { version = "0.51.0", features = ["duniter"] } +duniter-bca-types = { path = "types", features = ["duniter"] } +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +duniter-gva-db = { path = "../db" } +duniter-gva-dbs-reader = { path = "../dbs-reader" } +fast-threadpool = "0.2.3" +futures = "0.3.6" +once_cell = "1.5" +smallvec = { version = "1.4.0", features = ["serde", "write"] } +tokio = { version = "1.2", features = ["macros", "rt-multi-thread"] } +uninit = "0.4.0" + +[dev-dependencies] +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core", features = ["mem", "mock"] } +duniter-gva-dbs-reader = { path = "../dbs-reader", features = ["mock"] } +tokio = { version = "1.2", features = ["macros", "rt-multi-thread", "time"] } +mockall = "0.9.1" diff --git a/bca/src/exec_req_type.rs b/bca/src/exec_req_type.rs new file mode 100644 index 0000000000000000000000000000000000000000..183a3e5e678fef8d20490c7f9c7558b1690a1a96 --- /dev/null +++ b/bca/src/exec_req_type.rs @@ -0,0 +1,99 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +mod balances; +mod current_ud; +mod last_blockstamp_out_of_fork_window; +mod members_count; +mod prepare_simple_payment; +mod send_txs; +mod utxos; + +use dubp::crypto::keys::KeyPair; + +use crate::*; + +#[derive(Debug, PartialEq)] +pub(super) struct ExecReqTypeError(pub(super) String); + +impl<E> From<E> for ExecReqTypeError +where + E: ToString, +{ + fn from(e: E) -> Self { + Self(e.to_string()) + } +} + +pub(super) async fn execute_req_type( + bca_executor: &BcaExecutor, + req_type: BcaReqTypeV0, + _is_whitelisted: bool, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + match req_type { + BcaReqTypeV0::BalancesOfPubkeys(pubkeys) => { + balances::exec_req_balances_of_pubkeys(bca_executor, pubkeys).await + } + BcaReqTypeV0::FirstUtxosOfPubkeys { + amount_target_opt, + pubkeys, + } => utxos::exec_req_first_utxos_of_pubkeys(bca_executor, amount_target_opt, pubkeys).await, + BcaReqTypeV0::LastBlockstampOutOfForkWindow => { + last_blockstamp_out_of_fork_window::exec_req_last_blockstamp_out_of_fork_window( + bca_executor, + ) + .await + } + BcaReqTypeV0::MembersCount => members_count::exec_req_members_count(bca_executor).await, + BcaReqTypeV0::PrepareSimplePayment(params) => { + prepare_simple_payment::exec_req_prepare_simple_payment(bca_executor, params).await + } + BcaReqTypeV0::ProofServerPubkey { challenge } => Ok(BcaRespTypeV0::ProofServerPubkey { + challenge, + server_pubkey: bca_executor.self_keypair.public_key(), + sig: bca_executor + .self_keypair + .generate_signator() + .sign(&challenge), + }), + BcaReqTypeV0::Ping => Ok(BcaRespTypeV0::Pong), + BcaReqTypeV0::SendTxs(txs) => send_txs::send_txs(bca_executor, txs).await, + BcaReqTypeV0::Identities(pubkeys) => { + let dbs_reader = bca_executor.dbs_reader(); + Ok(BcaRespTypeV0::Identities( + bca_executor + .dbs_pool + .execute(move |dbs| { + pubkeys + .into_iter() + .map(|pubkey| { + dbs_reader.idty(&dbs.bc_db_ro, pubkey).map(|idty_opt| { + idty_opt.map(|idty| Identity { + is_member: idty.is_member, + username: idty.username, + }) + }) + }) + .collect::<KvResult<ArrayVec<_>>>() + }) + .await??, + )) + } + BcaReqTypeV0::CurrentUd => current_ud::exec_req_current_ud(bca_executor).await, + BcaReqTypeV0::BalancesOfScripts(scripts) => { + balances::exec_req_balances_of_scripts(bca_executor, scripts).await + } + } +} diff --git a/bca/src/exec_req_type/balances.rs b/bca/src/exec_req_type/balances.rs new file mode 100644 index 0000000000000000000000000000000000000000..6901a9d03e7f49015031fa2064acd9c18a1d28c0 --- /dev/null +++ b/bca/src/exec_req_type/balances.rs @@ -0,0 +1,61 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::{crypto::keys::ed25519::PublicKey, wallet::prelude::WalletScriptV10}; + +pub(super) async fn exec_req_balances_of_pubkeys( + bca_executor: &BcaExecutor, + pubkeys: ArrayVec<[PublicKey; 16]>, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + let dbs_reader = bca_executor.dbs_reader(); + Ok(BcaRespTypeV0::Balances( + bca_executor + .dbs_pool + .execute(move |_| { + pubkeys + .into_iter() + .map(|pubkey| { + dbs_reader + .get_account_balance(&WalletScriptV10::single_sig(pubkey)) + .map(|balance_opt| balance_opt.map(|balance| balance.0)) + }) + .collect::<Result<ArrayVec<_>, _>>() + }) + .await??, + )) +} + +pub(super) async fn exec_req_balances_of_scripts( + bca_executor: &BcaExecutor, + scripts: ArrayVec<[WalletScriptV10; 16]>, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + let dbs_reader = bca_executor.dbs_reader(); + Ok(BcaRespTypeV0::Balances( + bca_executor + .dbs_pool + .execute(move |_| { + scripts + .into_iter() + .map(|script| { + dbs_reader + .get_account_balance(&script) + .map(|balance_opt| balance_opt.map(|balance| balance.0)) + }) + .collect::<Result<ArrayVec<_>, _>>() + }) + .await??, + )) +} diff --git a/bca/src/exec_req_type/current_ud.rs b/bca/src/exec_req_type/current_ud.rs new file mode 100644 index 0000000000000000000000000000000000000000..02823dbbaf3d8a6b07cc623198c03511389bf5c9 --- /dev/null +++ b/bca/src/exec_req_type/current_ud.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(super) async fn exec_req_current_ud( + bca_executor: &BcaExecutor, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + if let Some(current_ud) = bca_executor + .cm_accessor + .get_current_meta(|cm| cm.current_ud) + .await + { + Ok(BcaRespTypeV0::CurrentUd(current_ud)) + } else { + Err("no blockchain".into()) + } +} diff --git a/bca/src/exec_req_type/last_blockstamp_out_of_fork_window.rs b/bca/src/exec_req_type/last_blockstamp_out_of_fork_window.rs new file mode 100644 index 0000000000000000000000000000000000000000..41529563e624d149e869d0f5abf85d903cbde4fc --- /dev/null +++ b/bca/src/exec_req_type/last_blockstamp_out_of_fork_window.rs @@ -0,0 +1,99 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::common::prelude::*; + +pub(super) async fn exec_req_last_blockstamp_out_of_fork_window( + bca_executor: &BcaExecutor, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + if let Some(current_block_number) = bca_executor + .cm_accessor + .get_current_meta(|cm| cm.current_block_meta.number) + .await + { + let dbs_reader = bca_executor.dbs_reader(); + bca_executor + .dbs_pool + .execute(move |dbs| { + let block_ref_number = if current_block_number < 101 { + 0 + } else { + current_block_number - 101 + }; + let block_ref_hash = dbs_reader + .block(&dbs.bc_db_ro, U32BE(block_ref_number))? + .expect("unreachable") + .hash; + Ok::<_, ExecReqTypeError>(BcaRespTypeV0::LastBlockstampOutOfForkWindow( + Blockstamp { + number: BlockNumber(block_ref_number), + hash: BlockHash(block_ref_hash), + }, + )) + }) + .await? + } else { + Err("no blockchain".into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[tokio::test] + async fn test_exec_req_last_blockstamp_out_of_fork_window_no_blockchain() { + let mut cm_mock = MockAsyncAccessor::new(); + cm_mock + .expect_get_current_meta::<u32>() + .times(1) + .returning(|_| None); + let dbs_reader = MockDbsReader::new(); + let bca_executor = + create_bca_executor(cm_mock, dbs_reader).expect("fail to create bca executor"); + + let resp_res = exec_req_last_blockstamp_out_of_fork_window(&bca_executor).await; + + assert_eq!(resp_res, Err(ExecReqTypeError("no blockchain".into()))); + } + + #[tokio::test] + async fn test_exec_req_last_blockstamp_out_of_fork_window_ok() -> Result<(), ExecReqTypeError> { + let mut cm_mock = MockAsyncAccessor::new(); + cm_mock + .expect_get_current_meta::<u32>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_block() + .times(1) + .returning(|_, _| Ok(Some(BlockMetaV2::default()))); + + let bca_executor = + create_bca_executor(cm_mock, dbs_reader).expect("fail to create bca executor"); + + let resp = exec_req_last_blockstamp_out_of_fork_window(&bca_executor).await?; + + assert_eq!( + resp, + BcaRespTypeV0::LastBlockstampOutOfForkWindow(Blockstamp::default()) + ); + + Ok(()) + } +} diff --git a/bca/src/exec_req_type/members_count.rs b/bca/src/exec_req_type/members_count.rs new file mode 100644 index 0000000000000000000000000000000000000000..71b85c6e3cadfc6fd22f3d9d90ec04c161d8c8ed --- /dev/null +++ b/bca/src/exec_req_type/members_count.rs @@ -0,0 +1,52 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(super) async fn exec_req_members_count( + bca_executor: &BcaExecutor, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + if let Some(members_count) = bca_executor + .cm_accessor + .get_current_meta(|cm| cm.current_block_meta.members_count) + .await + { + Ok(BcaRespTypeV0::MembersCount(members_count)) + } else { + Err("no blockchain".into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[tokio::test] + async fn test_exec_req_members_count() { + let mut cm_mock = MockAsyncAccessor::new(); + cm_mock + .expect_get_current_meta::<u64>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let dbs_reader = MockDbsReader::new(); + let bca_executor = + create_bca_executor(cm_mock, dbs_reader).expect("fail to create bca executor"); + + let resp_res = exec_req_members_count(&bca_executor).await; + + assert_eq!(resp_res, Ok(BcaRespTypeV0::MembersCount(0))); + } +} diff --git a/bca/src/exec_req_type/prepare_simple_payment.rs b/bca/src/exec_req_type/prepare_simple_payment.rs new file mode 100644 index 0000000000000000000000000000000000000000..a23a77650570d2ec5fcf652ff0ea00acf6519c44 --- /dev/null +++ b/bca/src/exec_req_type/prepare_simple_payment.rs @@ -0,0 +1,223 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software current_block_number: (), current_block_hash: (), inputs: (), inputs_sum: (): you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::wallet::prelude::*; +use duniter_bca_types::prepare_payment::{PrepareSimplePayment, PrepareSimplePaymentResp}; + +pub(super) async fn exec_req_prepare_simple_payment( + bca_executor: &BcaExecutor, + params: PrepareSimplePayment, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + let issuer = params.issuer; + + if let Some(current_meta) = bca_executor.cm_accessor.get_current_meta(|cm| *cm).await { + let current_block_meta = current_meta.current_block_meta; + let current_ud = current_meta.current_ud; + let dbs_reader = bca_executor.dbs_reader(); + let (amount, block_ref_number, block_ref_hash, (inputs, inputs_sum)) = bca_executor + .dbs_pool + .execute(move |dbs| { + let mut amount = params.amount.to_cents(current_ud); + let block_ref_number = if current_block_meta.number < 101 { + 0 + } else { + current_block_meta.number - 101 + }; + let block_ref_hash = dbs_reader + .block(&dbs.bc_db_ro, U32BE(block_ref_number))? + .expect("unreachable") + .hash; + let current_base = current_block_meta.unit_base as i64; + + if amount.base() > current_base { + Err("too long base".into()) + } else { + while amount.base() < current_base { + amount = amount.increment_base(); + } + Ok::<_, ExecReqTypeError>(( + amount, + block_ref_number, + block_ref_hash, + dbs_reader.find_inputs( + &dbs.bc_db_ro, + &dbs.txs_mp_db, + amount, + &WalletScriptV10::single(WalletConditionV10::Sig(issuer)), + false, + )?, + )) + } + }) + .await??; + + if inputs_sum < amount { + return Err("insufficient balance".into()); + } + + Ok(BcaRespTypeV0::PrepareSimplePayment( + PrepareSimplePaymentResp { + current_block_number: block_ref_number, + current_block_hash: block_ref_hash, + current_ud, + inputs, + inputs_sum, + }, + )) + } else { + Err("no blockchain".into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[tokio::test] + async fn test_exec_req_prepare_simple_payment_no_blockchain() { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<CurrentMeta>() + .times(1) + .returning(|_| None); + let dbs_reader = MockDbsReader::new(); + let bca_executor = + create_bca_executor(mock_cm, dbs_reader).expect("fail to create bca executor"); + + let resp_res = exec_req_prepare_simple_payment( + &bca_executor, + PrepareSimplePayment { + issuer: PublicKey::default(), + amount: Amount::Cents(SourceAmount::new(42, 0)), + }, + ) + .await; + + assert_eq!(resp_res, Err(ExecReqTypeError("no blockchain".into()))); + } + + #[tokio::test] + async fn test_exec_req_prepare_simple_payment_too_long_base() { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<CurrentMeta>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_block() + .times(1) + .returning(|_, _| Ok(Some(BlockMetaV2::default()))); + let bca_executor = + create_bca_executor(mock_cm, dbs_reader).expect("fail to create bca executor"); + + let resp_res = exec_req_prepare_simple_payment( + &bca_executor, + PrepareSimplePayment { + issuer: PublicKey::default(), + amount: Amount::Cents(SourceAmount::new(42, 1)), + }, + ) + .await; + + assert_eq!(resp_res, Err(ExecReqTypeError("too long base".into()))); + } + + #[tokio::test] + async fn test_exec_req_prepare_simple_payment_insufficient_balance() { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<CurrentMeta>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_block() + .times(1) + .returning(|_, _| Ok(Some(BlockMetaV2::default()))); + dbs_reader + .expect_find_inputs::<TxsMpV2Db<FileBackend>>() + .times(1) + .returning(|_, _, _, _, _| Ok((vec![], SourceAmount::default()))); + let bca_executor = + create_bca_executor(mock_cm, dbs_reader).expect("fail to create bca executor"); + + let resp_res = exec_req_prepare_simple_payment( + &bca_executor, + PrepareSimplePayment { + issuer: PublicKey::default(), + amount: Amount::Cents(SourceAmount::new(42, 0)), + }, + ) + .await; + + assert_eq!( + resp_res, + Err(ExecReqTypeError("insufficient balance".into())) + ); + } + + #[tokio::test] + async fn test_exec_req_prepare_simple_payment_ok() -> Result<(), ExecReqTypeError> { + let input = TransactionInputV10 { + amount: SourceAmount::with_base0(57), + id: SourceIdV10::Utxo(UtxoIdV10 { + tx_hash: Hash::default(), + output_index: 3, + }), + }; + + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<CurrentMeta>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_block() + .times(1) + .returning(|_, _| Ok(Some(BlockMetaV2::default()))); + dbs_reader + .expect_find_inputs::<TxsMpV2Db<FileBackend>>() + .times(1) + .returning(move |_, _, _, _, _| Ok((vec![input], SourceAmount::with_base0(57)))); + let bca_executor = + create_bca_executor(mock_cm, dbs_reader).expect("fail to create bca executor"); + + let resp = exec_req_prepare_simple_payment( + &bca_executor, + PrepareSimplePayment { + issuer: PublicKey::default(), + amount: Amount::Cents(SourceAmount::new(42, 0)), + }, + ) + .await?; + + assert_eq!( + resp, + BcaRespTypeV0::PrepareSimplePayment(PrepareSimplePaymentResp { + current_block_number: 0, + current_block_hash: Hash::default(), + current_ud: SourceAmount::ZERO, + inputs: vec![input], + inputs_sum: SourceAmount::with_base0(57), + }) + ); + + Ok(()) + } +} diff --git a/bca/src/exec_req_type/send_txs.rs b/bca/src/exec_req_type/send_txs.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb1ede18352b99476d87c264381f66209aff81d1 --- /dev/null +++ b/bca/src/exec_req_type/send_txs.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::{crypto::keys::KeyPair, documents::transaction::TransactionDocumentTrait}; +use duniter_bca_types::{ + rejected_tx::{RejectedTx, RejectedTxReason}, + Txs, +}; + +pub(super) async fn send_txs( + bca_executor: &BcaExecutor, + txs: Txs, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + let expected_currency = bca_executor.currency.clone(); + + let server_pubkey = bca_executor.self_keypair.public_key(); + let txs_mempool = bca_executor.txs_mempool; + + let mut rejected_txs = Vec::new(); + for (i, tx) in txs.into_iter().enumerate() { + if let Err(e) = tx.verify(Some(&expected_currency)) { + rejected_txs.push(RejectedTx { + tx_index: i as u16, + reason: RejectedTxReason::InvalidTx(e.to_string()), + }); + } else if let Err(rejected_tx) = bca_executor + .dbs_pool + .execute(move |dbs| { + txs_mempool + .add_pending_tx(&dbs.bc_db_ro, server_pubkey, &dbs.txs_mp_db, &tx) + .map_err(|e| RejectedTx { + tx_index: i as u16, + reason: match e { + duniter_core::mempools::TxMpError::Db(e) => { + RejectedTxReason::DbError(e.to_string()) + } + duniter_core::mempools::TxMpError::Full => { + RejectedTxReason::MempoolFull + } + duniter_core::mempools::TxMpError::TxAlreadyWritten => { + RejectedTxReason::TxAlreadyWritten + } + }, + }) + }) + .await? + { + rejected_txs.push(rejected_tx); + } + } + Ok(BcaRespTypeV0::RejectedTxs(rejected_txs)) +} diff --git a/bca/src/exec_req_type/utxos.rs b/bca/src/exec_req_type/utxos.rs new file mode 100644 index 0000000000000000000000000000000000000000..249ff1f48d8662940cf18a6fe1be639d685c4753 --- /dev/null +++ b/bca/src/exec_req_type/utxos.rs @@ -0,0 +1,58 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::{crypto::keys::ed25519::PublicKey, wallet::prelude::WalletScriptV10}; + +pub(super) async fn exec_req_first_utxos_of_pubkeys( + bca_executor: &BcaExecutor, + amount_target_opt: Option<Amount>, + pubkeys: ArrayVec<[PublicKey; 16]>, +) -> Result<BcaRespTypeV0, ExecReqTypeError> { + if let Some(current_ud) = bca_executor + .cm_accessor + .get_current_meta(|cm| cm.current_ud) + .await + { + let dbs_reader = bca_executor.dbs_reader(); + let scripts: ArrayVec<[WalletScriptV10; 16]> = pubkeys + .into_iter() + .map(WalletScriptV10::single_sig) + .collect(); + if let Some(amount_target) = amount_target_opt { + Ok(BcaRespTypeV0::FirstUtxosOfPubkeys( + bca_executor + .dbs_pool + .execute(move |_| { + Ok::<_, ExecReqTypeError>(dbs_reader.first_scripts_utxos( + Some(amount_target.to_cents(current_ud)), + 40, + &scripts, + )?) + }) + .await??, + )) + } else { + Ok(BcaRespTypeV0::FirstUtxosOfPubkeys( + bca_executor + .dbs_pool + .execute(move |_| dbs_reader.first_scripts_utxos(None, 40, &scripts)) + .await??, + )) + } + } else { + Err("no blockchain".into()) + } +} diff --git a/bca/src/lib.rs b/bca/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..d07777961fecb6ae28b0d018294962d85f11833c --- /dev/null +++ b/bca/src/lib.rs @@ -0,0 +1,412 @@ +// Copyright (C) 2020 Éloïs req_id: (), resp_type: ()SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +mod exec_req_type; + +const MAX_BATCH_SIZE: usize = 10; +const RESP_MIN_SIZE: usize = 64; +type RespBytes = SmallVec<[u8; RESP_MIN_SIZE]>; + +use crate::exec_req_type::ExecReqTypeError; +#[cfg(test)] +use crate::tests::AsyncAccessor; +use arrayvec::ArrayVec; +use async_bincode::AsyncBincodeReader; +use async_io_stream::IoStream; +use bincode::Options as _; +use dubp::crypto::keys::{ed25519::Ed25519KeyPair, Signator}; +use duniter_bca_types::{ + amount::Amount, bincode_opts, identity::Identity, BcaReq, BcaReqExecError, BcaReqTypeV0, + BcaResp, BcaRespTypeV0, BcaRespV0, +}; +pub use duniter_core::dbs::kv_typed::prelude::*; +use duniter_core::dbs::{FileBackend, SharedDbs}; +#[cfg(not(test))] +use duniter_core::global::AsyncAccessor; +use duniter_gva_dbs_reader::DbsReader; + +use futures::{prelude::stream::FuturesUnordered, StreamExt, TryStream, TryStreamExt}; +use once_cell::sync::OnceCell; +use smallvec::SmallVec; +use tokio::task::JoinError; + +#[cfg(test)] +use crate::tests::DbsReaderImpl; +#[cfg(not(test))] +use duniter_gva_dbs_reader::DbsReaderImpl; + +static BCA_EXECUTOR: OnceCell<BcaExecutor> = OnceCell::new(); + +pub fn set_bca_executor( + currency: String, + cm_accessor: AsyncAccessor, + dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + dbs_reader: DbsReaderImpl, + self_keypair: Ed25519KeyPair, + software_version: &'static str, + txs_mempool: duniter_core::mempools::TxsMempool, +) { + BCA_EXECUTOR + .set(BcaExecutor { + currency, + cm_accessor, + dbs_pool, + dbs_reader, + self_keypair, + software_version, + txs_mempool, + }) + .unwrap_or_else(|_| panic!("BCA_EXECUTOR already set !")) +} + +#[cfg(not(test))] +pub async fn execute<B, S>(query_body_stream: S, is_whitelisted: bool) -> Vec<u8> +where + B: AsRef<[u8]>, + S: 'static + TryStream<Ok = B, Error = std::io::Error> + Send + Unpin, +{ + unsafe { + BCA_EXECUTOR + .get_unchecked() + .execute(query_body_stream, is_whitelisted) + .await + } +} + +#[derive(Clone)] +struct BcaExecutor { + cm_accessor: AsyncAccessor, + currency: String, + dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + dbs_reader: DbsReaderImpl, + self_keypair: Ed25519KeyPair, + software_version: &'static str, + txs_mempool: duniter_core::mempools::TxsMempool, +} +use uninit::extension_traits::VecCapacity; +impl BcaExecutor { + pub async fn execute<B, S>(&self, query_body_stream: S, is_whitelisted: bool) -> Vec<u8> + where + B: AsRef<[u8]>, + S: 'static + TryStream<Ok = B, Error = std::io::Error> + Send + Unpin, + { + let async_bincode_reader = + AsyncBincodeReader::<IoStream<S, B>, BcaReq>::from(IoStream::new(query_body_stream)); + self.execute_inner(async_bincode_reader, is_whitelisted) + .await + .into_iter() + .fold(Vec::new(), |mut vec, elem| { + // Write resp len + let out = vec.reserve_uninit(4); + out.copy_from_slice(&u32::to_be_bytes(elem.len() as u32)[..]); + unsafe { + // # Safety + // + // - `.copy_from_slice()` contract guarantees initialization + // of `out`, which, in turn, from `reserve_uninit`'s contract, + // leads to the `vec` extra capacity having been initialized. + vec.set_len(vec.len() + 4); + } + + // Write resp content + let out = vec.reserve_uninit(elem.len()); + out.copy_from_slice(&elem[..]); + unsafe { + // # Safety + // + // - `.copy_from_slice()` contract guarantees initialization + // of `out`, which, in turn, from `reserve_uninit`'s contract, + // leads to the `vec` extra capacity having been initialized. + vec.set_len(vec.len() + elem.len()); + } + vec + }) + } + + async fn execute_inner( + &self, + stream: impl TryStream<Ok = BcaReq, Error = bincode::Error>, + is_whitelisted: bool, + ) -> Vec<RespBytes> { + match stream + .map_ok(|req| { + let self_clone = self.clone(); + tokio::spawn(async move { self_clone.execute_req(req, is_whitelisted).await }) + }) + .take(MAX_BATCH_SIZE) + .try_collect::<FuturesUnordered<_>>() + .await + { + Ok(futures_unordered) => { + futures_unordered + .map(|req_res: Result<BcaResp, JoinError>| { + let resp = match req_res { + Ok(resp) => Ok(resp), + Err(e) => Err(if e.is_cancelled() { + BcaReqExecError::Cancelled + } else if e.is_panic() { + BcaReqExecError::Panic + } else { + BcaReqExecError::Unknown + }), + }; + let mut resp_buffer = RespBytes::new(); + bincode_opts() + .serialize_into(&mut resp_buffer, &resp) + .expect("unreachable"); + resp_buffer + }) + .collect() + .await + } + Err(e) => { + let req_res: Result<BcaResp, BcaReqExecError> = + Err(BcaReqExecError::InvalidReq(e.to_string())); + let mut resp_buffer = RespBytes::new(); + bincode_opts() + .serialize_into(&mut resp_buffer, &req_res) + .expect("unreachable"); + vec![resp_buffer] + } + } + } + + #[inline(always)] + async fn execute_req(self, req: BcaReq, is_whitelisted: bool) -> BcaResp { + match req { + BcaReq::V0(req) => BcaResp::V0(BcaRespV0 { + req_id: req.req_id, + resp_type: match crate::exec_req_type::execute_req_type( + &self, + req.req_type, + is_whitelisted, + ) + .await + { + Ok(resp_type) => resp_type, + Err(e) => BcaRespTypeV0::Error(e.0), + }, + }), + _ => BcaResp::UnsupportedVersion, + } + } +} + +#[cfg(not(test))] +impl BcaExecutor { + #[inline(always)] + pub fn dbs_reader(&self) -> DbsReaderImpl { + self.dbs_reader + } +} + +#[cfg(test)] +mod tests { + use super::*; + pub use dubp::{ + block::prelude::*, + crypto::{ + hashs::Hash, + keys::{ed25519::PublicKey, KeyPair, Seed32}, + }, + documents::transaction::TransactionInputV10, + wallet::prelude::*, + }; + pub use duniter_bca_types::BcaReqV0; + pub use duniter_core::dbs::databases::bc_v2::{BcV2DbReadable, BcV2DbRo}; + pub use duniter_core::dbs::databases::cm_v1::{CmV1Db, CmV1DbReadable}; + pub use duniter_core::dbs::databases::txs_mp_v2::{TxsMpV2Db, TxsMpV2DbReadable}; + pub use duniter_core::dbs::BlockMetaV2; + pub use duniter_core::global::{CurrentMeta, MockAsyncAccessor}; + pub use duniter_gva_dbs_reader::MockDbsReader; + pub use futures::TryStreamExt; + + pub type AsyncAccessor = duniter_core::dbs::kv_typed::prelude::Arc<MockAsyncAccessor>; + pub type DbsReaderImpl = duniter_core::dbs::kv_typed::prelude::Arc<MockDbsReader>; + + impl BcaExecutor { + #[inline(always)] + pub fn dbs_reader(&self) -> DbsReaderImpl { + self.dbs_reader.clone() + } + } + + pub(crate) fn create_bca_executor( + mock_cm: MockAsyncAccessor, + mock_dbs_reader: MockDbsReader, + ) -> KvResult<BcaExecutor> { + let dbs = SharedDbs::mem()?; + let threadpool = + fast_threadpool::ThreadPool::start(fast_threadpool::ThreadPoolConfig::low(), dbs); + Ok(BcaExecutor { + cm_accessor: duniter_core::dbs::kv_typed::prelude::Arc::new(mock_cm), + currency: "g1".to_owned(), + dbs_pool: threadpool.into_async_handler(), + dbs_reader: duniter_core::dbs::kv_typed::prelude::Arc::new(mock_dbs_reader), + self_keypair: Ed25519KeyPair::from_seed( + Seed32::random().expect("fail to gen random seed"), + ), + software_version: "test", + txs_mempool: duniter_core::mempools::TxsMempool::new(10), + }) + } + + pub(crate) fn io_stream<B: AsRef<[u8]>>( + bytes: B, + ) -> impl TryStream<Ok = B, Error = std::io::Error> { + futures::stream::iter(std::iter::once(Ok(bytes))) + } + + #[tokio::test] + async fn test_one_req_ok() -> Result<(), bincode::Error> { + let req = BcaReq::V0(BcaReqV0 { + req_id: 42, + req_type: BcaReqTypeV0::MembersCount, + }); + assert_eq!(bincode_opts().serialized_size(&req)?, 3); + let mut bytes = [0u8; 7]; + + bincode_opts().serialize_into(&mut bytes[4..], &req)?; + bytes[3] = 3; + + use bincode::Options; + //println!("bytes_for_bincode={:?}", &bytes[4..]); + assert_eq!(req, bincode_opts().deserialize(&bytes[4..])?); + + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<u64>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let bca_executor = create_bca_executor(mock_cm, MockDbsReader::new()) + .expect("fail to create bca executor"); + + //println!("bytes={:?}", bytes); + let bytes_res = bca_executor.execute(io_stream(bytes), false).await; + //println!("bytes_res={:?}", bytes_res); + let bca_res: Vec<Result<BcaResp, BcaReqExecError>> = + AsyncBincodeReader::<_, Result<BcaResp, BcaReqExecError>>::from(&bytes_res[..]) + .try_collect::<Vec<_>>() + .await?; + + assert_eq!( + bca_res, + vec![Ok(BcaResp::V0(BcaRespV0 { + req_id: 42, + resp_type: BcaRespTypeV0::MembersCount(0) + }))] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_one_req_invalid() -> Result<(), bincode::Error> { + let req = BcaReq::V0(BcaReqV0 { + req_id: 42, + req_type: BcaReqTypeV0::MembersCount, + }); + assert_eq!(bincode_opts().serialized_size(&req)?, 3); + let mut bytes = [0u8; 7]; + + bincode_opts().serialize_into(&mut bytes[4..], &req)?; + bytes[3] = 2; + + use bincode::Options; + //println!("bytes_for_bincode={:?}", &bytes[4..]); + assert_eq!(req, bincode_opts().deserialize(&bytes[4..])?); + + let bca_executor = create_bca_executor(MockAsyncAccessor::new(), MockDbsReader::new()) + .expect("fail to create bca executor"); + + //println!("bytes={:?}", bytes); + let bytes_res = bca_executor.execute(io_stream(bytes), false).await; + //println!("bytes_res={:?}", bytes_res); + let bca_res: Vec<Result<BcaResp, BcaReqExecError>> = + AsyncBincodeReader::<_, Result<BcaResp, BcaReqExecError>>::from(&bytes_res[..]) + .try_collect::<Vec<_>>() + .await?; + + assert_eq!( + bca_res, + vec![Err(BcaReqExecError::InvalidReq( + "io error: unexpected end of file".to_owned() + ))] + ); + + Ok(()) + } + + #[tokio::test] + async fn test_two_reqs_ok() -> Result<(), bincode::Error> { + let req1 = BcaReq::V0(BcaReqV0 { + req_id: 42, + req_type: BcaReqTypeV0::Ping, + }); + assert_eq!(bincode_opts().serialized_size(&req1)?, 3); + let req2 = BcaReq::V0(BcaReqV0 { + req_id: 57, + req_type: BcaReqTypeV0::MembersCount, + }); + assert_eq!(bincode_opts().serialized_size(&req2)?, 3); + + let mut bytes = [0u8; 14]; + bincode_opts().serialize_into(&mut bytes[4..], &req1)?; + bytes[3] = 3; + bincode_opts().serialize_into(&mut bytes[11..], &req2)?; + bytes[10] = 3; + + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<u64>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let bca_executor = create_bca_executor(mock_cm, MockDbsReader::new()) + .expect("fail to create bca executor"); + + //println!("bytes={:?}", bytes); + let bytes_res = bca_executor.execute(io_stream(bytes), false).await; + //println!("bytes_res={:?}", bytes_res); + let bca_res: Vec<Result<BcaResp, BcaReqExecError>> = + AsyncBincodeReader::<_, Result<BcaResp, BcaReqExecError>>::from(&bytes_res[..]) + .try_collect::<Vec<_>>() + .await?; + + assert_eq!( + bca_res, + vec![ + Ok(BcaResp::V0(BcaRespV0 { + req_id: 42, + resp_type: BcaRespTypeV0::Pong + })), + Ok(BcaResp::V0(BcaRespV0 { + req_id: 57, + resp_type: BcaRespTypeV0::MembersCount(0) + })) + ] + ); + + Ok(()) + } +} diff --git a/bca/types/Cargo.toml b/bca/types/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ef4fc9023f20f9b92768a8f411ee1f404b6d9ecd --- /dev/null +++ b/bca/types/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "duniter-bca-types" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +license = "AGPL-3.0" +edition = "2018" + +[dependencies] +arrayvec = { version = "0.5.1", features = ["serde"] } +bincode = "1.3" +dubp = { version = "0.51.0" } +serde = { version = "1.0.105", features = ["derive"] } +smallvec = { version = "1.4.0", features = ["serde"] } +thiserror = "1.0.20" + +[features] +default = ["duniter"] + +client = ["dubp/client"] +duniter = ["dubp/duniter"] diff --git a/bca/types/src/amount.rs b/bca/types/src/amount.rs new file mode 100644 index 0000000000000000000000000000000000000000..1682a314190882c11e68425c33c087b509a0e49a --- /dev/null +++ b/bca/types/src/amount.rs @@ -0,0 +1,46 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum Amount { + Cents(SourceAmount), + Uds(f64), +} + +impl Default for Amount { + fn default() -> Self { + Self::Cents(SourceAmount::ZERO) + } +} + +impl Amount { + pub fn to_cents(self, ud_amount: SourceAmount) -> SourceAmount { + match self { + Amount::Cents(sa) => sa, + Amount::Uds(f64_) => { + if !f64_.is_finite() || f64_ <= 0f64 { + SourceAmount::ZERO + } else { + SourceAmount::new( + f64::round(ud_amount.amount() as f64 * f64_) as i64, + ud_amount.base(), + ) + } + } + } + } +} diff --git a/bca/types/src/identity.rs b/bca/types/src/identity.rs new file mode 100644 index 0000000000000000000000000000000000000000..e2302a9bfa7d9520b4a5b41831ff987276776ed8 --- /dev/null +++ b/bca/types/src/identity.rs @@ -0,0 +1,22 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct Identity { + pub is_member: bool, + pub username: String, +} diff --git a/bca/types/src/lib.rs b/bca/types/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..a9b987ba3fb79b635586838f529f12fae31f2497 --- /dev/null +++ b/bca/types/src/lib.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +pub mod amount; +pub mod identity; +pub mod prepare_payment; +pub mod rejected_tx; +pub mod utxo; + +use crate::amount::Amount; +use crate::identity::Identity; +use crate::prepare_payment::{PrepareSimplePayment, PrepareSimplePaymentResp}; +use crate::utxo::Utxo; + +use arrayvec::ArrayVec; +use bincode::Options as _; +use dubp::crypto::keys::ed25519::{PublicKey, Signature}; +use dubp::wallet::prelude::*; +use dubp::{common::prelude::Blockstamp, crypto::hashs::Hash}; +use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; +use thiserror::Error; + +// Constants + +pub const MAX_FIRST_UTXOS: usize = 40; + +// Request + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum BcaReq { + V0(BcaReqV0), + _V1, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct BcaReqV0 { + pub req_id: usize, + pub req_type: BcaReqTypeV0, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum BcaReqTypeV0 { + BalancesOfPubkeys(ArrayVec<[PublicKey; 16]>), + FirstUtxosOfPubkeys { + amount_target_opt: Option<Amount>, + pubkeys: ArrayVec<[PublicKey; 16]>, + }, + LastBlockstampOutOfForkWindow, + MembersCount, + PrepareSimplePayment(PrepareSimplePayment), + ProofServerPubkey { + challenge: [u8; 16], + }, + Ping, + SendTxs(Txs), + Identities(ArrayVec<[PublicKey; 16]>), + CurrentUd, + BalancesOfScripts(ArrayVec<[WalletScriptV10; 16]>), +} + +// Request types helpers + +pub type Txs = SmallVec<[dubp::documents::transaction::TransactionDocumentV10; 1]>; + +// Response + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum BcaResp { + V0(BcaRespV0), + UnsupportedVersion, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct BcaRespV0 { + pub req_id: usize, + pub resp_type: BcaRespTypeV0, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum BcaRespTypeV0 { + Error(String), + Balances(ArrayVec<[Option<SourceAmount>; 16]>), + FirstUtxosOfPubkeys(Vec<ArrayVec<[Utxo; MAX_FIRST_UTXOS]>>), + ProofServerPubkey { + challenge: [u8; 16], + server_pubkey: PublicKey, + sig: Signature, + }, + LastBlockstampOutOfForkWindow(Blockstamp), + MembersCount(u64), + PrepareSimplePayment(PrepareSimplePaymentResp), + Pong, + RejectedTxs(Vec<rejected_tx::RejectedTx>), + Identities(ArrayVec<[Option<Identity>; 16]>), + CurrentUd(SourceAmount), +} + +// Result and error + +pub type BcaResult = Result<BcaResp, BcaReqExecError>; + +#[derive(Clone, Debug, Deserialize, Error, PartialEq, Eq, Serialize)] +pub enum BcaReqExecError { + #[error("task cancelled")] + Cancelled, + #[error("Invalid request: {0}")] + InvalidReq(String), + #[error("task panicked")] + Panic, + #[error("Unknown error")] + Unknown, +} + +// Bincode configuration + +pub fn bincode_opts() -> impl bincode::Options { + bincode::options() + .with_limit(u32::max_value() as u64) + .allow_trailing_bytes() +} diff --git a/bca/types/src/prepare_payment.rs b/bca/types/src/prepare_payment.rs new file mode 100644 index 0000000000000000000000000000000000000000..2de5d621a5fb521be8a9304a9789602d6755523d --- /dev/null +++ b/bca/types/src/prepare_payment.rs @@ -0,0 +1,32 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::documents::transaction::TransactionInputV10; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct PrepareSimplePayment { + pub issuer: PublicKey, + pub amount: Amount, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct PrepareSimplePaymentResp { + pub current_block_number: u32, + pub current_block_hash: Hash, + pub current_ud: SourceAmount, + pub inputs: Vec<TransactionInputV10>, + pub inputs_sum: SourceAmount, +} diff --git a/bca/types/src/rejected_tx.rs b/bca/types/src/rejected_tx.rs new file mode 100644 index 0000000000000000000000000000000000000000..14b06e52b76e39ed5830541a422329093151535c --- /dev/null +++ b/bca/types/src/rejected_tx.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct RejectedTx { + pub tx_index: u16, + pub reason: RejectedTxReason, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum RejectedTxReason { + DbError(String), + InvalidTx(String), + MempoolFull, + TxAlreadyWritten, +} diff --git a/bca/types/src/utxo.rs b/bca/types/src/utxo.rs new file mode 100644 index 0000000000000000000000000000000000000000..d7716a808926b6f4fd449d5310ffdb60fb4d4fbb --- /dev/null +++ b/bca/types/src/utxo.rs @@ -0,0 +1,23 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct Utxo { + pub amount: SourceAmount, + pub tx_hash: Hash, + pub output_index: u8, +} diff --git a/db/Cargo.toml b/db/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..5296953084815395685b09e7ce1ede955cf37058 --- /dev/null +++ b/db/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "duniter-gva-db" +version = "0.1.0" +authors = ["elois <elois@duniter.org>"] +description = "Duniter GVA DB" +repository = "https://git.duniter.org/nodes/typescript/duniter" +license = "AGPL-3.0" +edition = "2018" + +[lib] +path = "src/lib.rs" + +[dependencies] +bincode = "1.2.1" +chrono = { version = "0.4.15", optional = true } +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +dubp = { version = "0.51.0", features = ["duniter"] } +kv_typed = { git = "https://git.duniter.org/nodes/rust/duniter-core", default-features = false, features = ["sled_backend"] } +parking_lot = "0.11.0" +paste = "1.0.2" +serde = { version = "1.0.105", features = ["derive"] } +serde_json = "1.0.53" +uninit = "0.4.0" +zerocopy = "0.3.0" + +[dev-dependencies] + +[features] +#default = ["explorer"] + +explorer = ["chrono", "duniter-core/explorer", "kv_typed/explorer"] +leveldb_backend = ["kv_typed/leveldb_backend"] diff --git a/db/src/keys.rs b/db/src/keys.rs new file mode 100644 index 0000000000000000000000000000000000000000..ced890746bed7f72d910bf66d2e3e0f26c3ea091 --- /dev/null +++ b/db/src/keys.rs @@ -0,0 +1,17 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod gva_utxo_id; +pub mod wallet_hash_with_bn; diff --git a/db/src/keys/gva_utxo_id.rs b/db/src/keys/gva_utxo_id.rs new file mode 100644 index 0000000000000000000000000000000000000000..1f624a872a284fc84f87581a30af18aed9decfc4 --- /dev/null +++ b/db/src/keys/gva_utxo_id.rs @@ -0,0 +1,166 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use uninit::prelude::*; + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct GvaUtxoIdDbV1([u8; 69]); // script hash ++ block_number ++ tx_hash ++ output_index + +impl Default for GvaUtxoIdDbV1 { + fn default() -> Self { + GvaUtxoIdDbV1([0u8; 69]) + } +} + +impl std::fmt::Display for GvaUtxoIdDbV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}:{}:{}", + self.get_script_hash(), + self.get_block_number(), + self.get_tx_hash(), + self.get_output_index() + ) + } +} + +impl GvaUtxoIdDbV1 { + pub fn get_script_hash(&self) -> Hash { + let mut buffer = uninit_array![u8; 32]; + + buffer.as_out().copy_from_slice(&self.0[..32]); + + Hash(unsafe { std::mem::transmute(buffer) }) + } + pub fn get_block_number(&self) -> u32 { + let mut buffer = uninit_array![u8; 4]; + + buffer.as_out().copy_from_slice(&self.0[32..36]); + + u32::from_be_bytes(unsafe { std::mem::transmute(buffer) }) + } + pub fn get_tx_hash(&self) -> Hash { + let mut buffer = uninit_array![u8; 32]; + + buffer.as_out().copy_from_slice(&self.0[36..68]); + + Hash(unsafe { std::mem::transmute(buffer) }) + } + pub fn get_output_index(&self) -> u8 { + self.0[68] + } + pub fn new( + script: WalletScriptV10, + block_number: u32, + tx_hash: Hash, + output_index: u8, + ) -> Self { + let script_hash = Hash::compute(script.to_string().as_bytes()); + Self::new_(script_hash, block_number, tx_hash, output_index) + } + pub fn new_(script_hash: Hash, block_number: u32, tx_hash: Hash, output_index: u8) -> Self { + // TODO uncomment when feature const_generics became stable ! + /*let mut buffer = uninit_array![u8; 69]; + let (hash_buffer, rest_buffer) = buffer.as_out().split_at_out(32); + let (bn_buffer, rest_buffer) = rest_buffer.split_at_out(4); + let (tx_hash_buffer, output_index_buffer) = rest_buffer.split_at_out(32); + hash_buffer.copy_from_slice(script_hash.as_ref()); + bn_buffer.copy_from_slice(&block_number.to_be_bytes()[..]); + tx_hash_buffer.copy_from_slice(tx_hash.as_ref()); + output_index_buffer.copy_from_slice(&[output_index]); + + Self(unsafe { std::mem::transmute(buffer) })*/ + let mut buffer = [0u8; 69]; + buffer[..32].copy_from_slice(script_hash.as_ref()); + buffer[32..36].copy_from_slice(&block_number.to_be_bytes()[..]); + buffer[36..68].copy_from_slice(tx_hash.as_ref()); + buffer[68] = output_index; + Self(buffer) + } + pub fn script_interval(script_hash: Hash) -> (Self, Self) { + let mut buffer = [0; 69]; + buffer[..32].copy_from_slice(script_hash.as_ref()); + let min = Self(buffer); + let mut buffer = [255; 69]; + buffer[..32].copy_from_slice(script_hash.as_ref()); + let max = Self(buffer); + + (min, max) + } + pub fn script_block_interval( + script_hash: Hash, + block_number_start: u32, + block_number_end: u32, + ) -> (Self, Self) { + ( + Self::new_(script_hash, block_number_start, Hash::default(), 0), + Self::new_(script_hash, block_number_end, Hash::max(), u8::MAX), + ) + } +} + +impl AsBytes for GvaUtxoIdDbV1 { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + f(&self.0[..]) + } +} + +impl FromBytes for GvaUtxoIdDbV1 { + type Err = CorruptedBytes; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + if bytes.len() == 69 { + // TODO uncomment when feature const_generics became stable ! + /*let mut buffer = uninit_array![u8; 69]; + buffer.as_out().copy_from_slice(bytes); + Ok(Self(unsafe { std::mem::transmute(buffer) }))*/ + let mut buffer = [0u8; 69]; + buffer.copy_from_slice(bytes); + Ok(Self(buffer)) + } else { + Err(CorruptedBytes("db corrupted".to_owned())) + } + } +} + +#[cfg(feature = "explorer")] +impl ExplorableKey for GvaUtxoIdDbV1 { + fn from_explorer_str(_: &str) -> std::result::Result<Self, FromExplorerKeyErr> { + unimplemented!() + } + fn to_explorer_string(&self) -> KvResult<String> { + Ok(self.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn utxo_gva_id_new() { + let script = WalletScriptV10::single(WalletConditionV10::Csv(86_400)); + let script_hash = Hash::compute(script.to_string().as_bytes()); + let tx_hash = Hash::default(); + let utxo_gva_id = GvaUtxoIdDbV1::new(script, 42, tx_hash, 3); + + assert_eq!(utxo_gva_id.get_script_hash(), script_hash); + assert_eq!(utxo_gva_id.get_block_number(), 42); + assert_eq!(utxo_gva_id.get_tx_hash(), tx_hash); + assert_eq!(utxo_gva_id.get_output_index(), 3); + } +} diff --git a/db/src/keys/wallet_hash_with_bn.rs b/db/src/keys/wallet_hash_with_bn.rs new file mode 100644 index 0000000000000000000000000000000000000000..bb5c767c1e0a12d63a760c97cdff072ea0f121ce --- /dev/null +++ b/db/src/keys/wallet_hash_with_bn.rs @@ -0,0 +1,119 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +use std::fmt::Display; +use uninit::prelude::*; + +#[derive( + Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, zerocopy::AsBytes, zerocopy::FromBytes, +)] +#[repr(transparent)] +pub struct WalletHashWithBnV1Db([u8; 36]); // wallet_hash ++ block_number + +impl WalletHashWithBnV1Db { + pub fn new(hash: Hash, block_number: BlockNumber) -> Self { + let mut buffer = uninit_array![u8; 36]; + let (hash_buffer, bn_buffer) = buffer.as_out().split_at_out(32); + + hash_buffer.copy_from_slice(hash.as_ref()); + bn_buffer.copy_from_slice(&block_number.0.to_be_bytes()[..]); + + Self(unsafe { std::mem::transmute(buffer) }) + } + pub fn get_wallet_hash(&self) -> Hash { + let mut buffer = uninit_array![u8; 32]; + + buffer.as_out().copy_from_slice(&self.0[..32]); + let bytes: [u8; 32] = unsafe { std::mem::transmute(buffer) }; + + Hash(bytes) + } + pub fn get_block_number(&self) -> u32 { + let mut buffer = uninit_array![u8; 4]; + + buffer.as_out().copy_from_slice(&self.0[32..]); + + u32::from_be_bytes(unsafe { std::mem::transmute(buffer) }) + } + pub fn wallet_hash_interval(wallet_hash: Hash) -> (Self, Self) { + ( + Self::new(wallet_hash, BlockNumber(0)), + Self::new(wallet_hash, BlockNumber(u32::MAX)), + ) + } +} + +impl Default for WalletHashWithBnV1Db { + fn default() -> Self { + WalletHashWithBnV1Db([0u8; 36]) + } +} + +impl Display for WalletHashWithBnV1Db { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.get_wallet_hash(), self.get_block_number()) + } +} + +impl AsBytes for WalletHashWithBnV1Db { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + f(self.0.as_ref()) + } +} + +impl kv_typed::prelude::FromBytes for WalletHashWithBnV1Db { + type Err = CorruptedBytes; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + let layout = zerocopy::LayoutVerified::<_, WalletHashWithBnV1Db>::new(bytes) + .ok_or_else(|| CorruptedBytes("corrupted db".to_owned()))?; + Ok(*layout) + } +} + +impl KeyZc for WalletHashWithBnV1Db { + type Ref = Self; +} + +impl ToDumpString for WalletHashWithBnV1Db { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableKey for WalletHashWithBnV1Db { + fn from_explorer_str(source: &str) -> Result<Self, FromExplorerKeyErr> { + let mut source = source.split(':'); + let hash_str = source + .next() + .ok_or_else(|| FromExplorerKeyErr("missing hash".into()))?; + let bn_str = source + .next() + .ok_or_else(|| FromExplorerKeyErr("missing block number".into()))?; + + let hash = Hash::from_hex(hash_str).map_err(|e| FromExplorerKeyErr(e.into()))?; + let block_number = bn_str + .parse() + .map_err(|e: std::num::ParseIntError| FromExplorerKeyErr(e.into()))?; + + Ok(WalletHashWithBnV1Db::new(hash, block_number)) + } + fn to_explorer_string(&self) -> KvResult<String> { + Ok(self.to_string()) + } +} diff --git a/db/src/lib.rs b/db/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..d1113e6243e5d60a4017e8625d016db9051499c5 --- /dev/null +++ b/db/src/lib.rs @@ -0,0 +1,71 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +mod keys; +mod values; + +pub use keys::gva_utxo_id::GvaUtxoIdDbV1; +pub use keys::wallet_hash_with_bn::WalletHashWithBnV1Db; +pub use values::gva_idty_db::GvaIdtyDbV1; +pub use values::gva_tx::GvaTxDbV1; +pub use values::wallet_script_array::WalletScriptArrayV2; + +pub(crate) use dubp::common::prelude::*; +pub(crate) use dubp::crypto::hashs::Hash; +pub(crate) use dubp::wallet::prelude::*; +pub(crate) use duniter_core::dbs::smallvec::SmallVec; +pub(crate) use duniter_core::dbs::{ + CorruptedBytes, HashKeyV2, PubKeyKeyV2, SourceAmountValV2, ToDumpString, WalletConditionsV2, +}; +pub(crate) use kv_typed::db_schema; +pub(crate) use kv_typed::prelude::*; +pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use std::collections::BTreeSet; + +db_schema!( + GvaV1, + [ + ["blocks_by_common_time", BlocksByCommonTime, U64BE, u32], + ["blocks_with_ud", BlocksWithUd, U32BE, ()], + ["blockchain_time", BlockchainTime, U32BE, u64], + ["txs", Txs, HashKeyV2, GvaTxDbV1], + ["txs_by_block", TxsByBlock, U32BE, Vec<Hash>], + ["txs_by_issuer", TxsByIssuer, WalletHashWithBnV1Db, BTreeSet<Hash>], + ["txs_by_recipient", TxsByRecipient, WalletHashWithBnV1Db, BTreeSet<Hash>], + [ + "scripts_by_pubkey", + ScriptsByPubkey, + PubKeyKeyV2, + WalletScriptArrayV2 + ], + [ + "gva_utxos", + GvaUtxos, + GvaUtxoIdDbV1, + SourceAmountValV2 + ], + ["balances", Balances, WalletConditionsV2, SourceAmountValV2], + ["gva_identities", GvaIdentities, PubKeyKeyV2, GvaIdtyDbV1], + ] +); diff --git a/db/src/values.rs b/db/src/values.rs new file mode 100644 index 0000000000000000000000000000000000000000..ed42095fa54010e5d37f4557d94d2f18d45df1d0 --- /dev/null +++ b/db/src/values.rs @@ -0,0 +1,18 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod gva_idty_db; +pub mod gva_tx; +pub mod wallet_script_array; diff --git a/db/src/values/gva_idty_db.rs b/db/src/values/gva_idty_db.rs new file mode 100644 index 0000000000000000000000000000000000000000..9bb918af86985f2ec7bd0d589404916cc01476be --- /dev/null +++ b/db/src/values/gva_idty_db.rs @@ -0,0 +1,54 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct GvaIdtyDbV1 { + pub is_member: bool, + pub joins: SmallVec<[BlockNumber; 2]>, + pub leaves: BTreeSet<BlockNumber>, + pub first_ud: Option<BlockNumber>, +} + +impl AsBytes for GvaIdtyDbV1 { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + f(&bincode::serialize(&self).unwrap_or_else(|_| unreachable!())) + } +} + +impl kv_typed::prelude::FromBytes for GvaIdtyDbV1 { + type Err = bincode::Error; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + bincode::deserialize(bytes) + } +} + +impl ToDumpString for GvaIdtyDbV1 { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableValue for GvaIdtyDbV1 { + fn from_explorer_str(source: &str) -> Result<Self, FromExplorerValueErr> { + serde_json::from_str(source).map_err(|e| FromExplorerValueErr(e.into())) + } + fn to_explorer_json(&self) -> KvResult<serde_json::Value> { + serde_json::to_value(&self).map_err(|e| KvError::DeserError(e.into())) + } +} diff --git a/db/src/values/gva_tx.rs b/db/src/values/gva_tx.rs new file mode 100644 index 0000000000000000000000000000000000000000..258fae77c228769021c44f7c0dd1357ef4d33977 --- /dev/null +++ b/db/src/values/gva_tx.rs @@ -0,0 +1,56 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +use dubp::documents::transaction::TransactionDocumentV10; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct GvaTxDbV1 { + pub tx: TransactionDocumentV10, + pub written_block: Blockstamp, + pub written_time: i64, +} + +impl AsBytes for GvaTxDbV1 { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + let bytes = bincode::serialize(self).unwrap_or_else(|_| unreachable!()); + f(bytes.as_ref()) + } +} + +impl kv_typed::prelude::FromBytes for GvaTxDbV1 { + type Err = CorruptedBytes; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + bincode::deserialize(&bytes).map_err(|e| CorruptedBytes(format!("{}: '{:?}'", e, bytes))) + } +} + +impl ToDumpString for GvaTxDbV1 { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableValue for GvaTxDbV1 { + fn from_explorer_str(source: &str) -> Result<Self, FromExplorerValueErr> { + Self::from_bytes(source.as_bytes()).map_err(|e| FromExplorerValueErr(e.0.into())) + } + fn to_explorer_json(&self) -> KvResult<serde_json::Value> { + serde_json::to_value(self).map_err(|e| KvError::DeserError(e.into())) + } +} diff --git a/db/src/values/wallet_script_array.rs b/db/src/values/wallet_script_array.rs new file mode 100644 index 0000000000000000000000000000000000000000..d0b4fa932549d1e14620c3274ced6295708d59c1 --- /dev/null +++ b/db/src/values/wallet_script_array.rs @@ -0,0 +1,53 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +#[derive(Debug, Default, PartialEq)] +pub struct WalletScriptArrayV2(pub std::collections::HashSet<WalletScriptV10>); + +impl AsBytes for WalletScriptArrayV2 { + fn as_bytes<T, F: FnMut(&[u8]) -> T>(&self, mut f: F) -> T { + f(&bincode::serialize(&self.0).unwrap_or_else(|_| unreachable!())) + } +} + +impl kv_typed::prelude::FromBytes for WalletScriptArrayV2 { + type Err = bincode::Error; + + fn from_bytes(bytes: &[u8]) -> std::result::Result<Self, Self::Err> { + Ok(Self(bincode::deserialize(bytes)?)) + } +} + +impl ToDumpString for WalletScriptArrayV2 { + fn to_dump_string(&self) -> String { + todo!() + } +} + +#[cfg(feature = "explorer")] +impl ExplorableValue for WalletScriptArrayV2 { + fn from_explorer_str(_: &str) -> std::result::Result<Self, FromExplorerValueErr> { + unimplemented!() + } + fn to_explorer_json(&self) -> KvResult<serde_json::Value> { + Ok(serde_json::Value::Array( + self.0 + .iter() + .map(|script| serde_json::Value::String(script.to_string())) + .collect(), + )) + } +} diff --git a/dbs-reader/Cargo.toml b/dbs-reader/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7b95b394baadd66cfb07a99b7e7d2e869f2aa58a --- /dev/null +++ b/dbs-reader/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "duniter-gva-dbs-reader" +version = "0.1.0" +authors = ["elois <elois@duniter.org>"] +description = "Duniter GVA DBs read operations" +repository = "https://git.duniter.org/nodes/typescript/duniter" +keywords = ["dubp", "duniter", "blockchain", "database"] +license = "AGPL-3.0" +edition = "2018" + +[lib] +path = "src/lib.rs" + +[features] +mock = ["mockall"] + +[dependencies] +anyhow = "1.0.34" +arrayvec = "0.5.1" +duniter-bca-types = { path = "../bca/types" } +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +duniter-gva-db = { path = "../db" } +dubp = { version = "0.51.0", features = ["duniter"] } +mockall = { version = "0.9.1", optional = true } +resiter = "0.4.0" + +[dev-dependencies] +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core", features = ["mem"] } +maplit = "1.0.2" +smallvec = { version = "1.4.0", features = ["serde", "write"] } +unwrap = "1.2.1" diff --git a/dbs-reader/src/block.rs b/dbs-reader/src/block.rs new file mode 100644 index 0000000000000000000000000000000000000000..4d7ce5d32e008b54d6d7fe6ac7728efefa40cdff --- /dev/null +++ b/dbs-reader/src/block.rs @@ -0,0 +1,228 @@ +// Copyright (C) 2021 Pascal Engélibert +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct BlockCursor { + pub number: BlockNumber, +} +impl std::fmt::Display for BlockCursor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.number) + } +} + +impl FromStr for BlockCursor { + type Err = WrongCursor; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(Self { + number: s.parse().map_err(|_| WrongCursor)?, + }) + } +} + +impl DbsReaderImpl { + pub(super) fn block_( + &self, + bc_db: &BcV2DbRo<FileBackend>, + number: U32BE, + ) -> KvResult<Option<duniter_core::dbs::BlockMetaV2>> { + bc_db.blocks_meta().get(&number) + } + + pub(super) fn blocks_( + &self, + bc_db: &BcV2DbRo<FileBackend>, + page_info: PageInfo<BlockCursor>, + ) -> KvResult<PagedData<Vec<(BlockCursor, duniter_core::dbs::BlockMetaV2)>>> { + let last_block_number = bc_db + .blocks_meta() + .iter_rev(.., |it| it.values().next_res())? + .ok_or_else(|| KvError::Custom("Empty blockchain".into()))? + .number; + + let first_cursor_opt = if page_info.not_all() { + Some(BlockCursor { + number: BlockNumber(0), + }) + } else { + None + }; + + let last_cursor_opt = if page_info.not_all() { + Some(BlockCursor { + number: BlockNumber(last_block_number), + }) + } else { + None + }; + + let k_min = U32BE(if page_info.order { + page_info.pos.map_or_else(|| 0, |pos| pos.number.0) + } else { + page_info.limit_opt.map_or_else( + || 0, + |limit| { + page_info + .pos + .map_or_else(|| last_block_number + 1, |pos| pos.number.0) + .saturating_sub(limit.get() as u32 - 1) + }, + ) + }); + let k_max = U32BE(if page_info.order { + page_info.limit_opt.map_or_else( + || last_block_number + 1, + |limit| { + page_info.pos.map_or_else( + || limit.get() as u32, + |pos| pos.number.0.saturating_add(limit.get() as u32), + ) + }, + ) + } else { + page_info.pos.map_or_else( + || last_block_number + 1, + |pos| pos.number.0.saturating_add(1), + ) + }); + + let blocks: Vec<(BlockCursor, duniter_core::dbs::BlockMetaV2)> = if page_info.order { + bc_db.blocks_meta().iter(k_min..k_max, blocks_inner)? + } else { + bc_db.blocks_meta().iter_rev(k_min..k_max, blocks_inner)? + }; + + Ok(PagedData { + has_next_page: has_next_page( + blocks + .iter() + .map(|(block_cursor, _block)| block_cursor.into()), + last_cursor_opt, + page_info, + page_info.order, + ), + has_previous_page: has_previous_page( + blocks + .iter() + .map(|(block_cursor, _block)| block_cursor.into()), + first_cursor_opt, + page_info, + page_info.order, + ), + data: blocks, + }) + } +} + +fn blocks_inner<I>(blocks_iter: I) -> KvResult<Vec<(BlockCursor, duniter_core::dbs::BlockMetaV2)>> +where + I: Iterator<Item = KvResult<(U32BE, BlockMetaV2)>>, +{ + blocks_iter + .map(|block_res| { + block_res.map(|block| { + ( + BlockCursor { + number: BlockNumber(block.0 .0), + }, + block.1, + ) + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_core::dbs::databases::bc_v2::BcV2DbWritable; + use std::num::NonZeroUsize; + + #[test] + fn test_block() -> KvResult<()> { + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let db_reader = DbsReaderImpl::mem(); + + bc_db + .blocks_meta_write() + .upsert(U32BE(0), duniter_core::dbs::BlockMetaV2::default())?; + + assert_eq!( + db_reader.block(&bc_db_ro, U32BE(0))?, + Some(duniter_core::dbs::BlockMetaV2::default()) + ); + + Ok(()) + } + + #[test] + fn test_blocks() -> KvResult<()> { + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let db_reader = DbsReaderImpl::mem(); + + for i in 0..20 { + bc_db.blocks_meta_write().upsert( + U32BE(i), + duniter_core::dbs::BlockMetaV2 { + number: i, + ..Default::default() + }, + )?; + } + + let blocks = db_reader.blocks( + &bc_db_ro, + PageInfo { + pos: Some(BlockCursor { + number: BlockNumber(10), + }), + order: true, + limit_opt: NonZeroUsize::new(3), + }, + )?; + + assert_eq!(blocks.data.len(), 3); + assert_eq!(blocks.data[0].1.number, 10); + assert_eq!(blocks.data[1].1.number, 11); + assert_eq!(blocks.data[2].1.number, 12); + assert!(blocks.has_previous_page); + assert!(blocks.has_next_page); + + let blocks = db_reader.blocks( + &bc_db_ro, + PageInfo { + pos: Some(BlockCursor { + number: BlockNumber(10), + }), + order: false, + limit_opt: NonZeroUsize::new(3), + }, + )?; + + assert_eq!(blocks.data.len(), 3); + assert_eq!(blocks.data[0].1.number, 10); + assert_eq!(blocks.data[1].1.number, 9); + assert_eq!(blocks.data[2].1.number, 8); + assert!(blocks.has_previous_page); + assert!(blocks.has_next_page); + + Ok(()) + } +} diff --git a/dbs-reader/src/current_frame.rs b/dbs-reader/src/current_frame.rs new file mode 100644 index 0000000000000000000000000000000000000000..872f2e9f5669836f357b9ec21fe09a90c5ef8b74 --- /dev/null +++ b/dbs-reader/src/current_frame.rs @@ -0,0 +1,33 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use duniter_core::dbs::BlockMetaV2; + +use crate::*; + +impl DbsReaderImpl { + pub(super) fn get_current_frame_<BcDb: 'static + BcV2DbReadable>( + &self, + bc_db: &BcDb, + current_block_meta: &BlockMetaV2, + ) -> anyhow::Result<Vec<BlockMetaV2>> { + let issuers_frame = current_block_meta.issuers_frame; + let start = U32BE(current_block_meta.number + 1 - issuers_frame as u32); + bc_db + .blocks_meta() + .iter_rev(start.., |it| it.values().collect::<KvResult<_>>()) + .map_err(Into::into) + } +} diff --git a/dbs-reader/src/find_inputs.rs b/dbs-reader/src/find_inputs.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac628ac355b2741e92a7a3255e3cf13322a18ea9 --- /dev/null +++ b/dbs-reader/src/find_inputs.rs @@ -0,0 +1,244 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::{ + uds_of_pubkey::UdsWithSum, + utxos::{UtxoCursor, UtxosWithSum}, + *, +}; +use dubp::{documents::transaction::TransactionInputV10, wallet::prelude::*}; + +pub(super) const MIN_AMOUNT: i64 = 100; + +impl DbsReaderImpl { + pub(super) fn find_inputs_<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + bc_db: &BcV2DbRo<FileBackend>, + txs_mp_db: &TxsMpDb, + amount: SourceAmount, + script: &WalletScriptV10, + use_mempool_sources: bool, + ) -> anyhow::Result<(Vec<TransactionInputV10>, SourceAmount)> { + // Pending UTXOs + let (mut inputs, mut inputs_sum) = if use_mempool_sources { + txs_mp_db + .outputs_by_script() + .get_ref_slice( + duniter_core::dbs::WalletConditionsV2::from_ref(script), + |utxos| { + let mut sum = SourceAmount::ZERO; + let inputs = utxos + .iter() + .filter(|utxo| { + !txs_mp_db + .utxos_ids() + .contains_key(&UtxoIdDbV2(*utxo.tx_hash(), utxo.output_index())) + .unwrap_or(true) + }) + .copied() + .map(|utxo| { + let amount = *utxo.amount(); + sum = sum + amount; + TransactionInputV10 { + amount, + id: SourceIdV10::Utxo(UtxoIdV10 { + tx_hash: *utxo.tx_hash(), + output_index: utxo.output_index() as usize, + }), + } + }) + .collect(); + + Ok((inputs, sum)) + }, + )? + .unwrap_or((Vec::with_capacity(500), SourceAmount::ZERO)) + } else { + (Vec::with_capacity(500), SourceAmount::ZERO) + }; + // UDs + if script.nodes.is_empty() { + if let WalletSubScriptV10::Single(WalletConditionV10::Sig(issuer)) = script.root { + let pending_uds_bn = txs_mp_db.uds_ids().iter(.., |it| { + it.keys() + .map_ok(|duniter_core::dbs::UdIdV2(_pk, bn)| bn) + .collect::<KvResult<_>>() + })?; + + let PagedData { + data: UdsWithSum { uds, sum: uds_sum }, + .. + } = self.unspent_uds_of_pubkey( + bc_db, + issuer, + PageInfo::default(), + Some(pending_uds_bn), + Some(amount - inputs_sum), + )?; + inputs.extend(uds.into_iter().map(|(block_number, source_amount)| { + TransactionInputV10 { + amount: source_amount, + id: SourceIdV10::Ud(UdSourceIdV10 { + issuer, + block_number, + }), + } + })); + inputs_sum = inputs_sum + uds_sum; + } + } + if inputs_sum < amount { + // Written UTXOs + let PagedData { + data: + UtxosWithSum { + utxos: written_utxos, + sum: written_utxos_sum, + }, + .. + } = self.find_script_utxos( + txs_mp_db, + Some(amount - inputs_sum), + PageInfo::default(), + &script, + )?; + inputs.extend(written_utxos.into_iter().map( + |( + UtxoCursor { + tx_hash, + output_index, + .. + }, + source_amount, + )| TransactionInputV10 { + amount: source_amount, + id: SourceIdV10::Utxo(UtxoIdV10 { + tx_hash, + output_index: output_index as usize, + }), + }, + )); + + Ok((inputs, inputs_sum + written_utxos_sum)) + } else { + Ok((inputs, inputs_sum)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_core::dbs::{ + databases::{bc_v2::BcV2DbWritable, txs_mp_v2::TxsMpV2DbWritable}, + BlockMetaV2, SourceAmountValV2, UdIdV2, UtxoIdDbV2, UtxoValV2, WalletConditionsV2, + }; + use duniter_gva_db::{GvaUtxoIdDbV1, GvaV1DbWritable}; + + const UD0: i64 = 100; + + #[test] + fn test_find_inputs() -> anyhow::Result<()> { + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + let txs_mp_db = + duniter_core::dbs::databases::txs_mp_v2::TxsMpV2Db::<Mem>::open(MemConf::default())?; + + let b0 = BlockMetaV2 { + dividend: Some(SourceAmount::with_base0(UD0)), + ..Default::default() + }; + let pk = PublicKey::default(); + let script = WalletScriptV10::single(WalletConditionV10::Sig(pk)); + let mut pending_utxos = BTreeSet::new(); + pending_utxos.insert(UtxoValV2::new( + SourceAmount::with_base0(900), + Hash::default(), + 10, + )); + + bc_db.blocks_meta_write().upsert(U32BE(0), b0)?; + bc_db + .uds_reval_write() + .upsert(U32BE(0), SourceAmountValV2(SourceAmount::with_base0(UD0)))?; + bc_db + .uds_write() + .upsert(UdIdV2(PublicKey::default(), BlockNumber(0)), ())?; + gva_db + .blockchain_time_write() + .upsert(U32BE(0), b0.median_time)?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(500)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(800)), + )?; + txs_mp_db + .outputs_by_script_write() + .upsert(WalletConditionsV2(script.clone()), pending_utxos)?; + + // Gen tx1 + let (inputs, inputs_sum) = db_reader.find_inputs( + &bc_db_ro, + &txs_mp_db, + SourceAmount::with_base0(550), + &script, + false, + )?; + assert_eq!(inputs.len(), 2); + assert_eq!(inputs_sum, SourceAmount::with_base0(600)); + + // Insert tx1 inputs in mempool + txs_mp_db + .uds_ids_write() + .upsert(UdIdV2(pk, BlockNumber(0)), ())?; + txs_mp_db + .utxos_ids_write() + .upsert(UtxoIdDbV2(Hash::default(), 0), ())?; + + // Gen tx2 + let (inputs, inputs_sum) = db_reader.find_inputs( + &bc_db_ro, + &txs_mp_db, + SourceAmount::with_base0(550), + &script, + false, + )?; + assert_eq!(inputs.len(), 1); + assert_eq!(inputs_sum, SourceAmount::with_base0(800)); + + // Insert tx2 inputs in mempool + txs_mp_db + .utxos_ids_write() + .upsert(UtxoIdDbV2(Hash::default(), 1), ())?; + + // Gen tx3 (use pending utxo) + let (inputs, inputs_sum) = db_reader.find_inputs( + &bc_db_ro, + &txs_mp_db, + SourceAmount::with_base0(750), + &script, + true, + )?; + assert_eq!(inputs.len(), 1); + assert_eq!(inputs_sum, SourceAmount::with_base0(900)); + + Ok(()) + } +} diff --git a/dbs-reader/src/idty.rs b/dbs-reader/src/idty.rs new file mode 100644 index 0000000000000000000000000000000000000000..705bca3ed8cb026132ecefb110b1f3b8c4320646 --- /dev/null +++ b/dbs-reader/src/idty.rs @@ -0,0 +1,70 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +impl DbsReaderImpl { + pub(super) fn idty_( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + ) -> KvResult<Option<duniter_core::dbs::IdtyDbV2>> { + bc_db.identities().get( + &duniter_core::dbs::PubKeyKeyV2::from_bytes(pubkey.as_ref()) + .map_err(|e| KvError::DeserError(Box::new(e)))?, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_core::dbs::databases::bc_v2::BcV2DbWritable; + + #[test] + fn test_idty() -> KvResult<()> { + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let db_reader = DbsReaderImpl::mem(); + let pk = PublicKey::default(); + + bc_db + .identities_write() + .upsert(PubKeyKeyV2(pk), duniter_core::dbs::IdtyDbV2::default())?; + + assert_eq!( + db_reader.idty(&bc_db_ro, pk)?, + Some(duniter_core::dbs::IdtyDbV2::default()) + ); + + bc_db.identities_write().upsert( + PubKeyKeyV2(pk), + duniter_core::dbs::IdtyDbV2 { + is_member: true, + username: String::from("JohnDoe"), + }, + )?; + + assert_eq!( + db_reader.idty(&bc_db_ro, pk)?, + Some(duniter_core::dbs::IdtyDbV2 { + is_member: true, + username: String::from("JohnDoe"), + }) + ); + + Ok(()) + } +} diff --git a/dbs-reader/src/lib.rs b/dbs-reader/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..738795fe494b61da661099eb770d86860a8a0623 --- /dev/null +++ b/dbs-reader/src/lib.rs @@ -0,0 +1,355 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +pub mod block; +pub mod current_frame; +pub mod find_inputs; +pub mod idty; +pub mod network; +pub mod pagination; +pub mod txs_history; +pub mod uds_of_pubkey; +pub mod utxos; + +pub use crate::pagination::{PageInfo, PagedData}; +pub use duniter_bca_types::MAX_FIRST_UTXOS; + +use crate::pagination::{has_next_page, has_previous_page}; +use arrayvec::ArrayVec; +use dubp::common::crypto::keys::ed25519::PublicKey; +use dubp::documents::transaction::TransactionDocumentV10; +use dubp::{block::DubpBlockV10, common::crypto::hashs::Hash}; +use dubp::{common::prelude::BlockNumber, wallet::prelude::*}; +use duniter_bca_types::utxo::Utxo; +use duniter_core::dbs::{databases::network_v1::NetworkV1DbReadable, FileBackend}; +use duniter_core::dbs::{ + databases::{ + bc_v2::{BcV2DbReadable, BcV2DbRo}, + cm_v1::CmV1DbReadable, + txs_mp_v2::TxsMpV2DbReadable, + }, + BlockMetaV2, +}; +use duniter_core::dbs::{ + kv_typed::prelude::*, HashKeyV2, PubKeyKeyV2, SourceAmountValV2, UtxoIdDbV2, +}; +use duniter_gva_db::{GvaIdtyDbV1, GvaTxDbV1, GvaUtxoIdDbV1, GvaV1DbReadable, GvaV1DbRo}; +use resiter::filter::Filter; +use resiter::filter_map::FilterMap; +use resiter::flatten::Flatten; +use resiter::map::Map; +use std::{ + collections::{BTreeSet, VecDeque}, + num::NonZeroUsize, + str::FromStr, +}; + +#[derive(Clone, Copy, Debug)] +pub struct WrongCursor; +impl std::fmt::Display for WrongCursor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "wrong cursor") + } +} +impl std::error::Error for WrongCursor {} + +#[cfg_attr(feature = "mock", mockall::automock)] +pub trait DbsReader { + fn all_uds_of_pubkey( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + ) -> KvResult<PagedData<uds_of_pubkey::UdsWithSum>>; + fn block(&self, bc_db: &BcV2DbRo<FileBackend>, number: U32BE) -> KvResult<Option<BlockMetaV2>>; + fn blocks( + &self, + bc_db: &BcV2DbRo<FileBackend>, + page_info: PageInfo<block::BlockCursor>, + ) -> KvResult<PagedData<Vec<(block::BlockCursor, BlockMetaV2)>>>; + fn endpoints<Db: 'static + NetworkV1DbReadable>( + &self, + network_db: &Db, + api_list: Vec<String>, + ) -> KvResult<Vec<String>>; + fn find_inputs<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + bc_db: &BcV2DbRo<FileBackend>, + txs_mp_db: &TxsMpDb, + amount: SourceAmount, + script: &WalletScriptV10, + use_mempool_sources: bool, + ) -> anyhow::Result<( + Vec<dubp::documents::transaction::TransactionInputV10>, + SourceAmount, + )>; + fn find_script_utxos<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + amount_target_opt: Option<SourceAmount>, + page_info: PageInfo<utxos::UtxoCursor>, + script: &WalletScriptV10, + ) -> anyhow::Result<PagedData<utxos::UtxosWithSum>>; + fn first_scripts_utxos( + &self, + amount_target_opt: Option<SourceAmount>, + first: usize, + scripts: &[WalletScriptV10], + ) -> anyhow::Result<Vec<arrayvec::ArrayVec<[Utxo; MAX_FIRST_UTXOS]>>>; + fn get_account_balance( + &self, + account_script: &WalletScriptV10, + ) -> KvResult<Option<SourceAmountValV2>>; + fn get_blockchain_time(&self, block_number: BlockNumber) -> anyhow::Result<u64>; + fn get_current_block<CmDb: 'static + CmV1DbReadable>( + &self, + cm_db: &CmDb, + ) -> KvResult<Option<DubpBlockV10>>; + fn get_current_frame<BcDb: 'static + BcV2DbReadable>( + &self, + bc_db: &BcDb, + current_block_meta: &BlockMetaV2, + ) -> anyhow::Result<Vec<BlockMetaV2>>; + fn get_txs_history_bc_received( + &self, + from: Option<u64>, + page_info: PageInfo<txs_history::TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<duniter_gva_db::GvaTxDbV1>>>; + fn get_txs_history_bc_sent( + &self, + from: Option<u64>, + page_info: PageInfo<txs_history::TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<duniter_gva_db::GvaTxDbV1>>>; + fn get_txs_history_mempool<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + pubkey: PublicKey, + ) -> KvResult<(Vec<TransactionDocumentV10>, Vec<TransactionDocumentV10>)>; + fn idty( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + ) -> KvResult<Option<duniter_core::dbs::IdtyDbV2>>; + fn peers_and_heads<DB: 'static + NetworkV1DbReadable>( + &self, + dunp_db: &DB, + ) -> KvResult< + Vec<( + duniter_core::dbs::PeerCardDbV1, + Vec<duniter_core::dbs::DunpHeadDbV1>, + )>, + >; + fn unspent_uds_of_pubkey( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + bn_to_exclude_opt: Option<std::collections::BTreeSet<BlockNumber>>, + amount_target_opt: Option<SourceAmount>, + ) -> KvResult<PagedData<uds_of_pubkey::UdsWithSum>>; +} + +#[derive(Clone, Copy, Debug)] +pub struct DbsReaderImpl(&'static GvaV1DbRo<FileBackend>); + +pub fn create_dbs_reader(gva_db_ro: &'static GvaV1DbRo<FileBackend>) -> DbsReaderImpl { + DbsReaderImpl(gva_db_ro) +} + +impl DbsReader for DbsReaderImpl { + fn all_uds_of_pubkey( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + ) -> KvResult<PagedData<uds_of_pubkey::UdsWithSum>> { + self.all_uds_of_pubkey_(bc_db, pubkey, page_info) + } + + fn block(&self, bc_db: &BcV2DbRo<FileBackend>, number: U32BE) -> KvResult<Option<BlockMetaV2>> { + self.block_(bc_db, number) + } + + fn blocks( + &self, + bc_db: &BcV2DbRo<FileBackend>, + page_info: PageInfo<block::BlockCursor>, + ) -> KvResult<PagedData<Vec<(block::BlockCursor, BlockMetaV2)>>> { + self.blocks_(bc_db, page_info) + } + + fn endpoints<Db: 'static + NetworkV1DbReadable>( + &self, + network_db: &Db, + api_list: Vec<String>, + ) -> KvResult<Vec<String>> { + self.endpoints_(network_db, api_list) + } + + fn find_inputs<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + bc_db: &BcV2DbRo<FileBackend>, + txs_mp_db: &TxsMpDb, + amount: SourceAmount, + script: &WalletScriptV10, + use_mempool_sources: bool, + ) -> anyhow::Result<( + Vec<dubp::documents::transaction::TransactionInputV10>, + SourceAmount, + )> { + self.find_inputs_(bc_db, txs_mp_db, amount, script, use_mempool_sources) + } + + fn find_script_utxos<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + amount_target_opt: Option<SourceAmount>, + page_info: PageInfo<utxos::UtxoCursor>, + script: &WalletScriptV10, + ) -> anyhow::Result<PagedData<utxos::UtxosWithSum>> { + self.find_script_utxos_(txs_mp_db_ro, amount_target_opt, page_info, script) + } + + fn first_scripts_utxos( + &self, + amount_target_opt: Option<SourceAmount>, + first: usize, + scripts: &[WalletScriptV10], + ) -> anyhow::Result<Vec<ArrayVec<[Utxo; MAX_FIRST_UTXOS]>>> { + self.first_scripts_utxos_(amount_target_opt, first, scripts) + } + + fn get_account_balance( + &self, + account_script: &WalletScriptV10, + ) -> KvResult<Option<SourceAmountValV2>> { + self.0 + .balances() + .get(duniter_core::dbs::WalletConditionsV2::from_ref( + account_script, + )) + } + + fn get_blockchain_time(&self, block_number: BlockNumber) -> anyhow::Result<u64> { + Ok(self + .0 + .blockchain_time() + .get(&U32BE(block_number.0))? + .unwrap_or_else(|| unreachable!())) + } + + fn get_current_block<CmDb: CmV1DbReadable>( + &self, + cm_db: &CmDb, + ) -> KvResult<Option<DubpBlockV10>> { + Ok(cm_db.current_block().get(&())?.map(|db_block| db_block.0)) + } + + fn get_current_frame<BcDb: 'static + BcV2DbReadable>( + &self, + bc_db: &BcDb, + current_block_meta: &BlockMetaV2, + ) -> anyhow::Result<Vec<BlockMetaV2>> { + self.get_current_frame_(bc_db, current_block_meta) + } + + fn get_txs_history_bc_received( + &self, + from: Option<u64>, + page_info: PageInfo<txs_history::TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<GvaTxDbV1>>> { + self.get_txs_history_bc_received_(from, page_info, script_hash, to) + } + + fn get_txs_history_bc_sent( + &self, + from: Option<u64>, + page_info: PageInfo<txs_history::TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<GvaTxDbV1>>> { + self.get_txs_history_bc_sent_(from, page_info, script_hash, to) + } + + fn get_txs_history_mempool<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + pubkey: PublicKey, + ) -> KvResult<(Vec<TransactionDocumentV10>, Vec<TransactionDocumentV10>)> { + self.get_txs_history_mempool_(txs_mp_db_ro, pubkey) + } + + fn idty( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + ) -> KvResult<Option<duniter_core::dbs::IdtyDbV2>> { + self.idty_(bc_db, pubkey) + } + + fn peers_and_heads<DB: 'static + NetworkV1DbReadable>( + &self, + dunp_db: &DB, + ) -> KvResult< + Vec<( + duniter_core::dbs::PeerCardDbV1, + Vec<duniter_core::dbs::DunpHeadDbV1>, + )>, + > { + self.peers_and_heads_(dunp_db) + } + + fn unspent_uds_of_pubkey( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + bn_to_exclude_opt: Option<BTreeSet<BlockNumber>>, + amount_target_opt: Option<SourceAmount>, + ) -> KvResult<PagedData<uds_of_pubkey::UdsWithSum>> { + self.unspent_uds_of_pubkey_( + bc_db, + pubkey, + page_info, + bn_to_exclude_opt.as_ref(), + amount_target_opt, + ) + } +} + +#[cfg(test)] +impl DbsReaderImpl { + pub(crate) fn mem() -> Self { + use duniter_gva_db::GvaV1DbWritable; + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default()) + .expect("fail to create memory gva db"); + create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }) + } +} diff --git a/dbs-reader/src/network.rs b/dbs-reader/src/network.rs new file mode 100644 index 0000000000000000000000000000000000000000..4ebe0aaf2a1dfa0f06f9a138d46bf999ea0689b5 --- /dev/null +++ b/dbs-reader/src/network.rs @@ -0,0 +1,200 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::crypto::keys::PublicKey as _; +use duniter_core::dbs::{databases::network_v1::NetworkV1DbReadable, DunpHeadDbV1, PeerCardDbV1}; + +#[allow(clippy::unnecessary_wraps)] +impl DbsReaderImpl { + pub(super) fn endpoints_<DB: NetworkV1DbReadable>( + &self, + network_db: &DB, + mut api_list: Vec<String>, + ) -> KvResult<Vec<String>> { + if api_list.is_empty() { + return Ok(vec![]); + } + for api in &mut api_list { + api.push(' '); + } + network_db.peers_old().iter(.., |it| { + it.values() + .map_ok(|peer| { + peer.endpoints.into_iter().filter(|endpoint| { + api_list + .iter() + .any(|api| endpoint.starts_with(api.as_str())) + }) + }) + .flatten_ok() + .collect::<Result<Vec<String>, _>>() + }) + } + pub(super) fn peers_and_heads_<DB: NetworkV1DbReadable>( + &self, + dunp_db: &DB, + ) -> KvResult<Vec<(PeerCardDbV1, Vec<DunpHeadDbV1>)>> { + Ok(dunp_db.peers_old().iter(.., |it| { + it.values() + .filter_map(|peer_res| { + if let Ok(peer) = peer_res { + if let Ok(pubkey) = PublicKey::from_base58(&peer.pubkey) { + let k_min = duniter_core::dbs::DunpNodeIdV1Db::new(0, pubkey); + let k_max = duniter_core::dbs::DunpNodeIdV1Db::new(u32::MAX, pubkey); + Some(( + peer, + dunp_db.heads_old().iter(k_min..k_max, |it| { + it.values().filter_map(|head| head.ok()).collect() + }), + )) + } else { + None + } + } else { + None + } + }) + .collect() + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_core::dbs::databases::network_v1::NetworkV1DbWritable; + use duniter_core::dbs::PeerCardDbV1; + + #[test] + fn test_empty_endpoints() -> KvResult<()> { + // Populate DB + let dunp_db = + duniter_core::dbs::databases::network_v1::NetworkV1Db::<Mem>::open(MemConf::default())?; + let db_reader = DbsReaderImpl::mem(); + let pk = PublicKey::default(); + + dunp_db + .peers_old_write() + .upsert(PubKeyKeyV2(pk), PeerCardDbV1::default())?; + + // Request Data + let api_list = vec!["GVA".to_owned()]; + assert_eq!( + db_reader.endpoints_(&dunp_db, api_list)?, + Vec::<String>::new() + ); + + Ok(()) + } + #[test] + fn test_endpoints_with_empty_api_list() -> KvResult<()> { + let dummy_endpoint = "GVA S domain.tld 443 gva"; + + // Populate DB + let dunp_db = + duniter_core::dbs::databases::network_v1::NetworkV1Db::<Mem>::open(MemConf::default())?; + let db_reader = DbsReaderImpl::mem(); + let pk = PublicKey::default(); + let peer = PeerCardDbV1 { + endpoints: vec![dummy_endpoint.to_owned()], + ..Default::default() + }; + + dunp_db.peers_old_write().upsert(PubKeyKeyV2(pk), peer)?; + + // Request Data + let api_list = vec![]; + assert_eq!( + db_reader.endpoints_(&dunp_db, api_list)?, + Vec::<String>::new() + ); + + Ok(()) + } + #[test] + fn test_single_peer_endpoints() -> KvResult<()> { + let dummy_endpoint = "GVA S domain.tld 443 gva"; + + // Populate DB + let dunp_db = + duniter_core::dbs::databases::network_v1::NetworkV1Db::<Mem>::open(MemConf::default())?; + let db_reader = DbsReaderImpl::mem(); + let pk = PublicKey::default(); + let peer = PeerCardDbV1 { + endpoints: vec![dummy_endpoint.to_owned()], + ..Default::default() + }; + + dunp_db.peers_old_write().upsert(PubKeyKeyV2(pk), peer)?; + + // Request Data + let api_list = vec!["GVA".to_owned()]; + assert_eq!( + db_reader.endpoints_(&dunp_db, api_list)?, + vec![dummy_endpoint.to_owned()] + ); + + Ok(()) + } + + #[test] + fn test_peers_and_heads() -> KvResult<()> { + let dunp_db = + duniter_core::dbs::databases::network_v1::NetworkV1Db::<Mem>::open(MemConf::default())?; + let db_reader = DbsReaderImpl::mem(); + let pk = PublicKey::default(); + + dunp_db.peers_old_write().upsert( + PubKeyKeyV2(pk), + PeerCardDbV1 { + pubkey: pk.to_string(), + ..Default::default() + }, + )?; + dunp_db.heads_old_write().upsert( + duniter_core::dbs::DunpNodeIdV1Db::new(42, pk), + DunpHeadDbV1::default(), + )?; + dunp_db.heads_old_write().upsert( + duniter_core::dbs::DunpNodeIdV1Db::new(43, pk), + DunpHeadDbV1 { + pubkey: PublicKey::from_base58("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .expect("invalid pubkey"), + ..Default::default() + }, + )?; + + assert_eq!( + db_reader.peers_and_heads(&dunp_db)?, + vec![( + PeerCardDbV1 { + pubkey: pk.to_string(), + ..Default::default() + }, + vec![ + DunpHeadDbV1::default(), + DunpHeadDbV1 { + pubkey: PublicKey::from_base58("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + .expect("invalid pubkey"), + ..Default::default() + } + ] + )] + ); + + Ok(()) + } +} diff --git a/dbs-reader/src/pagination.rs b/dbs-reader/src/pagination.rs new file mode 100644 index 0000000000000000000000000000000000000000..48d80bbc52110b02474c321f6acfa9477df5332c --- /dev/null +++ b/dbs-reader/src/pagination.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Debug)] +pub struct PagedData<D: std::fmt::Debug> { + pub data: D, + pub has_previous_page: bool, + pub has_next_page: bool, +} +impl<D: std::fmt::Debug + Default> PagedData<D> { + pub fn empty() -> Self { + PagedData { + data: D::default(), + has_previous_page: false, + has_next_page: false, + } + } +} + +#[derive(Debug)] +pub struct PageInfo<T> { + pub(crate) pos: Option<T>, + /// Order: true for ASC, false for DESC + pub(crate) order: bool, + pub(crate) limit_opt: Option<NonZeroUsize>, +} +impl<T> PageInfo<T> { + pub fn new(pos: Option<T>, order: bool, limit_opt: Option<NonZeroUsize>) -> Self { + PageInfo { + pos, + order, + limit_opt, + } + } + pub fn limit_opt(&self) -> Option<NonZeroUsize> { + self.limit_opt + } + pub fn not_all(&self) -> bool { + self.limit_opt.is_some() || self.pos.is_some() + } + pub fn order(&self) -> bool { + self.order + } + pub fn pos(&self) -> Option<&T> { + self.pos.as_ref() + } +} +impl<T> Default for PageInfo<T> { + fn default() -> Self { + PageInfo { + pos: None, + order: true, + limit_opt: None, + } + } +} +impl<T> Clone for PageInfo<T> +where + T: Clone, +{ + fn clone(&self) -> Self { + Self { + pos: self.pos.clone(), + order: self.order, + limit_opt: self.limit_opt, + } + } +} +impl<T> Copy for PageInfo<T> where T: Copy {} + +pub(crate) fn has_next_page< + 'i, + C: 'static + std::fmt::Debug + Default + Ord, + I: DoubleEndedIterator<Item = OwnedOrRef<'i, C>>, +>( + mut page_cursors: I, + last_cursor_opt: Option<C>, + page_info: PageInfo<C>, + page_not_reversed: bool, +) -> bool { + if page_info.not_all() { + if let Some(last_cursor) = last_cursor_opt { + //println!("TMP last_cursor={:?}", last_cursor); + if let Some(page_end_cursor) = if page_not_reversed { + page_cursors.next_back() + } else { + page_cursors.next() + } { + //println!("TMP page_end_cursor={:?}", page_end_cursor); + page_end_cursor.as_ref() != &last_cursor + } else { + page_info.pos.unwrap_or_default() < last_cursor + } + } else { + false + } + } else { + false + } +} + +pub(crate) fn has_previous_page< + 'i, + C: 'static + std::fmt::Debug + Default + Ord, + I: DoubleEndedIterator<Item = OwnedOrRef<'i, C>>, +>( + mut page_cursors: I, + first_cursor_opt: Option<C>, + page_info: PageInfo<C>, + page_not_reversed: bool, +) -> bool { + if page_info.not_all() { + if let Some(first_cursor) = first_cursor_opt { + //println!("TMP first_cursor={:?}", first_cursor); + if let Some(page_start_cursor) = if page_not_reversed { + page_cursors.next() + } else { + page_cursors.next_back() + } { + page_start_cursor.as_ref() != &first_cursor + } else { + page_info.pos.unwrap_or_default() > first_cursor + } + } else { + false + } + } else { + false + } +} diff --git a/dbs-reader/src/txs_history.rs b/dbs-reader/src/txs_history.rs new file mode 100644 index 0000000000000000000000000000000000000000..8182961290e809d64fc9979260315069652468b6 --- /dev/null +++ b/dbs-reader/src/txs_history.rs @@ -0,0 +1,836 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use duniter_core::dbs::smallvec::SmallVec; +use duniter_gva_db::WalletHashWithBnV1Db; + +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct TxBcCursor { + pub block_number: BlockNumber, + pub tx_hash: Hash, +} +impl std::fmt::Display for TxBcCursor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.block_number, self.tx_hash,) + } +} + +impl FromStr for TxBcCursor { + type Err = WrongCursor; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut s = s.split(':'); + let block_number = s + .next() + .ok_or(WrongCursor)? + .parse() + .map_err(|_| WrongCursor)?; + let tx_hash = Hash::from_hex(s.next().ok_or(WrongCursor)?).map_err(|_| WrongCursor)?; + Ok(Self { + block_number, + tx_hash, + }) + } +} + +impl DbsReaderImpl { + pub(super) fn get_txs_history_bc_received_( + &self, + from: Option<u64>, + page_info: PageInfo<TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<GvaTxDbV1>>> { + let mut start_k = WalletHashWithBnV1Db::new( + script_hash, + BlockNumber(if let Some(from) = from { + self.0 + .blocks_by_common_time() + .iter(U64BE(from).., |it| it) + .values() + .next_res()? + .unwrap_or(u32::MAX) + } else { + 0 + }), + ); + let mut end_k = WalletHashWithBnV1Db::new( + script_hash, + BlockNumber(if let Some(to) = to { + self.0 + .blocks_by_common_time() + .iter_rev(..U64BE(to), |it| it) + .values() + .next_res()? + .unwrap_or(0) + } else { + u32::MAX + }), + ); + let first_cursor_opt = if page_info.not_all() { + self.0 + .txs_by_recipient() + .iter_ref_slice(start_k..=end_k, |k, hashs| { + Ok(TxBcCursor { + block_number: BlockNumber(k.get_block_number()), + tx_hash: hashs[0], + }) + }) + .next_res()? + } else { + None + }; + let last_cursor_opt = if page_info.not_all() { + self.0 + .txs_by_recipient() + .iter_ref_slice_rev(start_k..=end_k, |k, hashs| { + Ok(TxBcCursor { + block_number: BlockNumber(k.get_block_number()), + tx_hash: hashs[hashs.len() - 1], + }) + }) + .next_res()? + } else { + None + }; + let first_hashs_opt = if let Some(TxBcCursor { + block_number, + tx_hash: hash_limit, + }) = page_info.pos + { + if page_info.order { + let hashs = self.0.txs_by_recipient().get_ref_slice( + &WalletHashWithBnV1Db::new(script_hash, block_number), + |hashs| { + Ok(hashs + .iter() + .rev() + .take_while(|hash| *hash != &hash_limit) + .copied() + .collect::<SmallVec<[Hash; 8]>>()) + }, + )?; + start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(block_number.0 + 1)); + hashs + } else { + let hashs = self.0.txs_by_recipient().get_ref_slice( + &WalletHashWithBnV1Db::new(script_hash, block_number), + |hashs| { + Ok(hashs + .iter() + .take_while(|hash| *hash != &hash_limit) + .copied() + .collect::<SmallVec<[Hash; 8]>>()) + }, + )?; + if block_number == BlockNumber(0) { + return Ok(PagedData::empty()); + } + end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(block_number.0 - 1)); + hashs + } + } else { + None + }; + + if page_info.order { + let txs_iter = self + .0 + .txs_by_recipient() + .iter_ref_slice(start_k..=end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 8]>::new(); + for hash in hashs { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok(); + txs_history_bc_collect( + *self, + first_cursor_opt, + first_hashs_opt, + last_cursor_opt, + page_info, + txs_iter, + ) + } else { + let txs_iter = self + .0 + .txs_by_recipient() + .iter_ref_slice_rev(start_k..=end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 8]>::new(); + for hash in hashs.iter().rev() { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok(); + txs_history_bc_collect( + *self, + first_cursor_opt, + first_hashs_opt, + last_cursor_opt, + page_info, + txs_iter, + ) + } + } + pub(super) fn get_txs_history_bc_sent_( + &self, + from: Option<u64>, + page_info: PageInfo<TxBcCursor>, + script_hash: Hash, + to: Option<u64>, + ) -> KvResult<PagedData<VecDeque<GvaTxDbV1>>> { + let mut start_k = WalletHashWithBnV1Db::new( + script_hash, + BlockNumber(if let Some(from) = from { + self.0 + .blocks_by_common_time() + .iter(U64BE(from).., |it| it) + .values() + .next_res()? + .unwrap_or(u32::MAX) + } else { + 0 + }), + ); + let mut end_k = WalletHashWithBnV1Db::new( + script_hash, + BlockNumber(if let Some(to) = to { + self.0 + .blocks_by_common_time() + .iter_rev(..U64BE(to), |it| it) + .values() + .next_res()? + .unwrap_or(0) + } else { + u32::MAX + }), + ); + let first_cursor_opt = if page_info.not_all() { + self.0 + .txs_by_issuer() + .iter_ref_slice(start_k..=end_k, |k, hashs| { + Ok(TxBcCursor { + block_number: BlockNumber(k.get_block_number()), + tx_hash: hashs[0], + }) + }) + .next_res()? + } else { + None + }; + let last_cursor_opt = if page_info.not_all() { + self.0 + .txs_by_issuer() + .iter_ref_slice_rev(start_k..=end_k, |k, hashs| { + Ok(TxBcCursor { + block_number: BlockNumber(k.get_block_number()), + tx_hash: hashs[hashs.len() - 1], + }) + }) + .next_res()? + } else { + None + }; + let first_hashs_opt = if let Some(TxBcCursor { + block_number, + tx_hash: hash_limit, + }) = page_info.pos + { + if page_info.order { + let hashs = self.0.txs_by_issuer().get_ref_slice( + &WalletHashWithBnV1Db::new(script_hash, block_number), + |hashs| { + Ok(hashs + .iter() + .rev() + .take_while(|hash| *hash != &hash_limit) + .copied() + .collect::<SmallVec<[Hash; 8]>>()) + }, + )?; + start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(block_number.0 + 1)); + hashs + } else { + let hashs = self.0.txs_by_issuer().get_ref_slice( + &WalletHashWithBnV1Db::new(script_hash, block_number), + |hashs| { + Ok(hashs + .iter() + .take_while(|hash| *hash != &hash_limit) + .copied() + .collect::<SmallVec<[Hash; 8]>>()) + }, + )?; + if block_number == BlockNumber(0) { + return Ok(PagedData::empty()); + } + end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(block_number.0 - 1)); + hashs + } + } else { + None + }; + + if page_info.order { + let txs_iter = self + .0 + .txs_by_issuer() + .iter_ref_slice(start_k..=end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 8]>::new(); + for hash in hashs { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok(); + txs_history_bc_collect( + *self, + first_cursor_opt, + first_hashs_opt, + last_cursor_opt, + page_info, + txs_iter, + ) + } else { + let txs_iter = self + .0 + .txs_by_issuer() + .iter_ref_slice_rev(start_k..=end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 8]>::new(); + for hash in hashs.iter().rev() { + if let Some(tx_db) = self.0.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok(); + txs_history_bc_collect( + *self, + first_cursor_opt, + first_hashs_opt, + last_cursor_opt, + page_info, + txs_iter, + ) + } + } + pub(super) fn get_txs_history_mempool_<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + pubkey: PublicKey, + ) -> KvResult<(Vec<TransactionDocumentV10>, Vec<TransactionDocumentV10>)> { + let sending = txs_mp_db_ro + .txs_by_issuer() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut sent = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db.0); + } + } + Ok(sent) + })? + .unwrap_or_default(); + let pending = txs_mp_db_ro + .txs_by_recipient() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut pending = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + pending.push(tx_db.0); + } + } + Ok(pending) + })? + .unwrap_or_default(); + Ok((sending, pending)) + } +} + +fn txs_history_bc_collect<I: Iterator<Item = KvResult<GvaTxDbV1>>>( + dbs_reader: DbsReaderImpl, + first_cursor_opt: Option<TxBcCursor>, + first_hashs_opt: Option<SmallVec<[Hash; 8]>>, + last_cursor_opt: Option<TxBcCursor>, + page_info: PageInfo<TxBcCursor>, + txs_iter: I, +) -> KvResult<PagedData<VecDeque<GvaTxDbV1>>> { + let mut txs = if let Some(limit) = page_info.limit_opt { + txs_iter + .take(limit.get()) + .collect::<KvResult<VecDeque<_>>>()? + } else { + txs_iter.collect::<KvResult<VecDeque<_>>>()? + }; + + if let Some(first_hashs) = first_hashs_opt { + for hash in first_hashs.into_iter() { + if let Some(tx_db) = dbs_reader.0.txs().get(&HashKeyV2(hash))? { + txs.push_front(tx_db); + } + } + } + + Ok(PagedData { + has_next_page: if page_info.order { + has_next_page( + txs.iter().map(|tx_db| { + TxBcCursor { + block_number: tx_db.written_block.number, + tx_hash: tx_db.tx.get_hash(), + } + .into() + }), + last_cursor_opt, + page_info, + page_info.order, + ) + } else { + // Server can't efficiently determine hasNextPage in DESC order + false + }, + has_previous_page: if page_info.order { + // Server can't efficiently determine hasPreviousPage in ASC order + false + } else { + has_previous_page( + txs.iter().map(|tx_db| { + TxBcCursor { + block_number: tx_db.written_block.number, + tx_hash: tx_db.tx.get_hash(), + } + .into() + }), + first_cursor_opt, + page_info, + page_info.order, + ) + }, + data: txs, + }) +} + +// Needed for BMA only +pub struct TxsHistory { + pub sent: Vec<GvaTxDbV1>, + pub received: Vec<GvaTxDbV1>, + pub sending: Vec<TransactionDocumentV10>, + pub pending: Vec<TransactionDocumentV10>, +} + +// Needed for BMA only +pub fn get_transactions_history_for_bma<GvaDb: GvaV1DbReadable, TxsMpDb: TxsMpV2DbReadable>( + gva_db_ro: &GvaDb, + txs_mp_db_ro: &TxsMpDb, + pubkey: PublicKey, +) -> KvResult<TxsHistory> { + let script_hash = Hash::compute(WalletScriptV10::single_sig(pubkey).to_string().as_bytes()); + let start_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(0)); + let end_k = WalletHashWithBnV1Db::new(script_hash, BlockNumber(u32::MAX)); + + let sent = gva_db_ro + .txs_by_issuer() + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 2]>::new(); + for hash in hashs { + if let Some(tx_db) = gva_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>()?; + + let received = gva_db_ro + .txs_by_recipient() + .iter_ref_slice(start_k..end_k, |_k, hashs| { + let mut sent = SmallVec::<[GvaTxDbV1; 2]>::new(); + for hash in hashs { + if let Some(tx_db) = gva_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db); + } + } + Ok(sent) + }) + .flatten_ok() + .collect::<KvResult<Vec<_>>>()?; + let sending = txs_mp_db_ro + .txs_by_issuer() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut sent = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + sent.push(tx_db.0); + } + } + Ok(sent) + })? + .unwrap_or_default(); + let pending = txs_mp_db_ro + .txs_by_recipient() + .get_ref_slice(&PubKeyKeyV2(pubkey), |hashs| { + let mut pending = Vec::with_capacity(hashs.len()); + for hash in hashs { + if let Some(tx_db) = txs_mp_db_ro.txs().get(HashKeyV2::from_ref(hash))? { + pending.push(tx_db.0); + } + } + Ok(pending) + })? + .unwrap_or_default(); + Ok(TxsHistory { + sent, + received, + sending, + pending, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use dubp::{ + common::prelude::{BlockHash, Blockstamp}, + crypto::keys::ed25519::PublicKey, + documents::transaction::{TransactionDocumentV10, TransactionDocumentV10Stringified}, + documents_parser::prelude::FromStringObject, + }; + use duniter_gva_db::GvaV1DbWritable; + use maplit::btreeset; + use unwrap::unwrap; + + fn gen_tx(hash: Hash, written_block_number: BlockNumber) -> GvaTxDbV1 { + GvaTxDbV1 { + tx: unwrap!(TransactionDocumentV10::from_string_object( + &TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: + "1-0000000000000000000000000000000000000000000000000000000000000000" + .to_owned(), + locktime: 0, + issuers: vec![], + inputs: vec![], + unlocks: vec![], + outputs: vec![], + comment: "".to_owned(), + signatures: vec![], + hash: Some(hash.to_hex()), + } + )), + written_block: Blockstamp { + number: written_block_number, + hash: BlockHash(Hash::default()), + }, + written_time: 1, + } + } + + #[test] + fn test_get_txs_history_bc_received() -> KvResult<()> { + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + + let s1 = WalletScriptV10::single_sig(PublicKey::default()); + let s1_hash = Hash::compute(&s1.to_string().as_bytes()); + + gva_db.txs_write().upsert( + HashKeyV2(Hash::default()), + gen_tx(Hash::default(), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([1; 32])), + gen_tx(Hash([1; 32]), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([2; 32])), + gen_tx(Hash([2; 32]), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([3; 32])), + gen_tx(Hash([3; 32]), BlockNumber(1)), + )?; + gva_db.txs_by_recipient_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(1)), + btreeset![Hash::default(), Hash([1; 32]), Hash([2; 32]), Hash([3; 32])], + )?; + gva_db.blocks_by_common_time_write().upsert(U64BE(1), 1)?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([4; 32])), + gen_tx(Hash([4; 32]), BlockNumber(2)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([5; 32])), + gen_tx(Hash([5; 32]), BlockNumber(2)), + )?; + gva_db.txs_by_recipient_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(2)), + btreeset![Hash([4; 32]), Hash([5; 32])], + )?; + gva_db.blocks_by_common_time_write().upsert(U64BE(2), 2)?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([6; 32])), + gen_tx(Hash([6; 32]), BlockNumber(3)), + )?; + gva_db.txs_by_recipient_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(3)), + btreeset![Hash([6; 32])], + )?; + gva_db.blocks_by_common_time_write().upsert(U64BE(3), 3)?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([7; 32])), + gen_tx(Hash([7; 32]), BlockNumber(4)), + )?; + gva_db.txs_by_recipient_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(4)), + btreeset![Hash([7; 32])], + )?; + gva_db.blocks_by_common_time_write().upsert(U64BE(4), 4)?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([8; 32])), + gen_tx(Hash([8; 32]), BlockNumber(5)), + )?; + gva_db.txs_by_recipient_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(5)), + btreeset![Hash([8; 32])], + )?; + gva_db.blocks_by_common_time_write().upsert(U64BE(5), 5)?; + + /*let received = db_reader.get_txs_history_bc_received( + PageInfo { + order: true, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([1; 32]), + block_number: BlockNumber(1), + }), + }, + s1_hash, + )?; + assert_eq!( + received.data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![Hash([2; 32]), Hash([3; 32]), Hash([4; 32]), Hash([5; 32])], + ); + assert!(!received.has_next_page); + assert!(!received.has_previous_page); + + let received = db_reader.get_txs_history_bc_received( + PageInfo { + order: false, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([1; 32]), + block_number: BlockNumber(1), + }), + }, + s1_hash, + )?; + assert_eq!( + received.data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![Hash([0; 32])], + ); + assert!(!received.has_next_page); + assert!(!received.has_previous_page);*/ + + let received = db_reader.get_txs_history_bc_received( + None, + PageInfo { + order: false, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([5; 32]), + block_number: BlockNumber(2), + }), + }, + s1_hash, + None, + )?; + assert_eq!( + received + .data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![ + Hash([4; 32]), + Hash([3; 32]), + Hash([2; 32]), + Hash([1; 32]), + Hash([0; 32]), + ], + ); + assert!(!received.has_next_page); + assert!(!received.has_previous_page); + + let received = db_reader.get_txs_history_bc_received( + Some(2), + PageInfo { + order: true, + limit_opt: None, + pos: None, + }, + s1_hash, + Some(5), + )?; + assert_eq!( + received + .data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![Hash([4; 32]), Hash([5; 32]), Hash([6; 32]), Hash([7; 32])], + ); + assert!(!received.has_next_page); + assert!(!received.has_previous_page); + + Ok(()) + } + + #[test] + fn test_get_txs_history_bc_sent() -> KvResult<()> { + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + + let s1 = WalletScriptV10::single_sig(PublicKey::default()); + let s1_hash = Hash::compute(&s1.to_string().as_bytes()); + + gva_db.txs_write().upsert( + HashKeyV2(Hash::default()), + gen_tx(Hash::default(), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([1; 32])), + gen_tx(Hash([1; 32]), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([2; 32])), + gen_tx(Hash([2; 32]), BlockNumber(1)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([3; 32])), + gen_tx(Hash([3; 32]), BlockNumber(1)), + )?; + gva_db.txs_by_issuer_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(1)), + btreeset![Hash::default(), Hash([1; 32]), Hash([2; 32]), Hash([3; 32])], + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([4; 32])), + gen_tx(Hash([4; 32]), BlockNumber(2)), + )?; + gva_db.txs_write().upsert( + HashKeyV2(Hash([5; 32])), + gen_tx(Hash([5; 32]), BlockNumber(2)), + )?; + gva_db.txs_by_issuer_write().upsert( + WalletHashWithBnV1Db::new(s1_hash, BlockNumber(2)), + btreeset![Hash([4; 32]), Hash([5; 32])], + )?; + + let sent = db_reader.get_txs_history_bc_sent( + None, + PageInfo { + order: true, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([1; 32]), + block_number: BlockNumber(1), + }), + }, + s1_hash, + None, + )?; + assert_eq!( + sent.data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![Hash([2; 32]), Hash([3; 32]), Hash([4; 32]), Hash([5; 32])], + ); + assert!(!sent.has_next_page); + assert!(!sent.has_previous_page); + + let sent = db_reader.get_txs_history_bc_sent( + None, + PageInfo { + order: false, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([1; 32]), + block_number: BlockNumber(1), + }), + }, + s1_hash, + None, + )?; + assert_eq!( + sent.data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![Hash([0; 32])], + ); + assert!(!sent.has_next_page); + assert!(!sent.has_previous_page); + + let sent = db_reader.get_txs_history_bc_sent( + None, + PageInfo { + order: false, + limit_opt: None, + pos: Some(TxBcCursor { + tx_hash: Hash([5; 32]), + block_number: BlockNumber(2), + }), + }, + s1_hash, + None, + )?; + assert_eq!( + sent.data + .into_iter() + .map(|tx_db| tx_db.tx.get_hash()) + .collect::<Vec<_>>(), + vec![ + Hash([4; 32]), + Hash([3; 32]), + Hash([2; 32]), + Hash([1; 32]), + Hash([0; 32]), + ], + ); + assert!(!sent.has_next_page); + assert!(!sent.has_previous_page); + + Ok(()) + } +} diff --git a/dbs-reader/src/uds_of_pubkey.rs b/dbs-reader/src/uds_of_pubkey.rs new file mode 100644 index 0000000000000000000000000000000000000000..e92d3411dab1f5be8bf19606ae0136eccb87996b --- /dev/null +++ b/dbs-reader/src/uds_of_pubkey.rs @@ -0,0 +1,829 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use duniter_core::dbs::smallvec::SmallVec; +use duniter_core::dbs::{ + databases::bc_v2::{UdsEvent, UdsRevalEvent}, + UdIdV2, +}; + +#[derive(Debug, Default)] +pub struct UdsWithSum { + pub uds: Vec<(BlockNumber, SourceAmount)>, + pub sum: SourceAmount, +} + +impl DbsReaderImpl { + pub(super) fn all_uds_of_pubkey_( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + ) -> KvResult<PagedData<UdsWithSum>> { + ( + bc_db.uds_reval(), + self.0.blocks_with_ud(), + self.0.gva_identities(), + ) + .read(|(uds_reval, blocks_with_ud, gva_identities)| { + if let Some(gva_idty) = gva_identities.get(&PubKeyKeyV2(pubkey))? { + match page_info.pos { + None => { + if page_info.order { + blocks_with_ud.iter(.., move |it| { + all_uds_of_pubkey_inner::<FileBackend, _>( + gva_idty, + page_info, + it.keys().map_ok(|bn| BlockNumber(bn.0)), + uds_reval, + None, + ) + }) + } else { + let last_ud_opt = + blocks_with_ud.iter_rev(.., |it| it.keys().next_res())?; + blocks_with_ud.iter_rev(.., move |it| { + all_uds_of_pubkey_inner::<FileBackend, _>( + gva_idty, + page_info, + it.keys().map_ok(|bn| BlockNumber(bn.0)), + uds_reval, + last_ud_opt.map(|bn| BlockNumber(bn.0)), + ) + }) + } + } + Some(pos) => { + if page_info.order { + blocks_with_ud.iter(U32BE(pos.0).., move |it| { + all_uds_of_pubkey_inner::<FileBackend, _>( + gva_idty, + page_info, + it.keys().map_ok(|bn| BlockNumber(bn.0)), + uds_reval, + None, + ) + }) + } else { + let last_ud_opt = + blocks_with_ud.iter_rev(.., |it| it.keys().next_res())?; + blocks_with_ud.iter_rev(..=U32BE(pos.0), move |it| { + all_uds_of_pubkey_inner::<FileBackend, _>( + gva_idty, + page_info, + it.keys().map_ok(|bn| BlockNumber(bn.0)), + uds_reval, + last_ud_opt.map(|bn| BlockNumber(bn.0)), + ) + }) + } + } + } + } else { + Ok(PagedData::empty()) + } + }) + } + + pub(super) fn unspent_uds_of_pubkey_( + &self, + bc_db: &BcV2DbRo<FileBackend>, + pubkey: PublicKey, + page_info: PageInfo<BlockNumber>, + bn_to_exclude_opt: Option<&BTreeSet<BlockNumber>>, + amount_target_opt: Option<SourceAmount>, + ) -> KvResult<PagedData<UdsWithSum>> { + (bc_db.uds(), bc_db.uds_reval()).read(|(uds, uds_reval)| { + let (first_ud_opt, last_ud_opt) = if page_info.not_all() { + get_first_and_last_unspent_ud(&uds, pubkey, bn_to_exclude_opt)? + } else { + (None, None) + }; + let mut blocks_numbers = if let Some(pos) = page_info.pos { + if page_info.order { + uds.iter( + UdIdV2(pubkey, pos)..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| { + let it = it.keys().map_ok(|UdIdV2(_p, bn)| bn); + if let Some(bn_to_exclude) = bn_to_exclude_opt { + it.filter_ok(|bn| !bn_to_exclude.contains(&bn)) + .collect::<KvResult<Vec<_>>>() + } else { + it.collect::<KvResult<Vec<_>>>() + } + }, + )? + } else { + uds.iter_rev(UdIdV2(pubkey, BlockNumber(0))..=UdIdV2(pubkey, pos), |it| { + let it = it.keys().map_ok(|UdIdV2(_p, bn)| bn); + if let Some(bn_to_exclude) = bn_to_exclude_opt { + it.filter_ok(|bn| !bn_to_exclude.contains(&bn)) + .collect::<KvResult<Vec<_>>>() + } else { + it.collect::<KvResult<Vec<_>>>() + } + })? + } + } else if page_info.order { + uds.iter( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| { + let it = it.keys().map_ok(|UdIdV2(_p, bn)| bn); + if let Some(bn_to_exclude) = bn_to_exclude_opt { + it.filter_ok(|bn| !bn_to_exclude.contains(&bn)) + .collect::<KvResult<Vec<_>>>() + } else { + it.collect::<KvResult<Vec<_>>>() + } + }, + )? + } else { + uds.iter_rev( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| { + let it = it.keys().map_ok(|UdIdV2(_p, bn)| bn); + if let Some(bn_to_exclude) = bn_to_exclude_opt { + it.filter_ok(|bn| !bn_to_exclude.contains(&bn)) + .collect::<KvResult<Vec<_>>>() + } else { + it.collect::<KvResult<Vec<_>>>() + } + }, + )? + }; + + if blocks_numbers.is_empty() { + Ok(PagedData::empty()) + } else { + if let Some(limit) = page_info.limit_opt { + blocks_numbers.truncate(limit.get()); + } + let first_block_number = if page_info.order { + blocks_numbers[0] + } else { + blocks_numbers[blocks_numbers.len() - 1] + }; + let first_reval = uds_reval + .iter_rev(..=U32BE(first_block_number.0), |it| it.keys().next_res())? + .expect("corrupted db"); + let blocks_numbers_len = blocks_numbers.len(); + let blocks_numbers = blocks_numbers.into_iter(); + let uds_with_sum = if page_info.order { + collect_uds( + blocks_numbers, + blocks_numbers_len, + first_reval, + uds_reval, + amount_target_opt, + )? + } else { + collect_uds( + blocks_numbers.rev(), + blocks_numbers_len, + first_reval, + uds_reval, + amount_target_opt, + )? + }; + Ok(PagedData { + has_previous_page: has_previous_page( + uds_with_sum.uds.iter().map(|(bn, _sa)| bn.into()), + first_ud_opt, + page_info, + true, + ), + has_next_page: has_next_page( + uds_with_sum.uds.iter().map(|(bn, _sa)| bn.into()), + last_ud_opt, + page_info, + true, + ), + data: uds_with_sum, + }) + } + }) + } +} + +fn all_uds_of_pubkey_inner<B, I>( + gva_idty: GvaIdtyDbV1, + page_info: PageInfo<BlockNumber>, + blocks_with_ud: I, + uds_reval: TxColRo<B::Col, UdsRevalEvent>, + last_ud_opt: Option<BlockNumber>, +) -> KvResult<PagedData<UdsWithSum>> +where + B: Backend, + I: Iterator<Item = KvResult<BlockNumber>>, +{ + let first_ud = gva_idty.first_ud; + let mut blocks_numbers = filter_blocks_numbers(gva_idty, page_info, blocks_with_ud)?; + + if blocks_numbers.is_empty() { + return Ok(PagedData::empty()); + } + + let not_reach_end = if page_info.order { + if let Some(limit) = page_info.limit_opt { + if blocks_numbers.len() <= limit.get() { + false + } else { + blocks_numbers.pop(); + true + } + } else { + false + } + } else if let Some(last_ud) = last_ud_opt { + blocks_numbers[0] != last_ud + } else { + false + }; + let blocks_numbers_len = blocks_numbers.len(); + + let first_block_number = if page_info.order { + blocks_numbers[0] + } else { + blocks_numbers[blocks_numbers_len - 1] + }; + + let first_reval = uds_reval + .iter_rev(..=U32BE(first_block_number.0), |it| it.keys().next_res())? + .expect("corrupted db"); + + let uds_with_sum = if page_info.order { + collect_uds( + blocks_numbers.into_iter(), + blocks_numbers_len, + first_reval, + uds_reval, + None, + )? + } else { + collect_uds( + blocks_numbers.into_iter().rev(), + blocks_numbers_len, + first_reval, + uds_reval, + None, + )? + }; + + Ok(PagedData { + has_previous_page: has_previous_page( + uds_with_sum.uds.iter().map(|(bn, _sa)| bn.into()), + first_ud, + page_info, + true, + ), + has_next_page: not_reach_end, + data: uds_with_sum, + }) +} + +fn filter_blocks_numbers<I: Iterator<Item = KvResult<BlockNumber>>>( + gva_idty: GvaIdtyDbV1, + page_info: PageInfo<BlockNumber>, + blocks_with_ud: I, +) -> KvResult<Vec<BlockNumber>> { + let mut is_member_changes = SmallVec::<[BlockNumber; 4]>::new(); + for (join, leave) in gva_idty.joins.iter().zip(gva_idty.leaves.iter()) { + is_member_changes.push(*join); + is_member_changes.push(*leave); + } + if gva_idty.joins.len() > gva_idty.leaves.len() { + is_member_changes.push(*gva_idty.joins.last().unwrap_or_else(|| unreachable!())); + } + + if page_info.order { + let mut i = 0; + let mut is_member = false; + if let Some(limit) = page_info.limit_opt { + blocks_with_ud + .filter_ok(|bn| { + while i < is_member_changes.len() && *bn >= is_member_changes[i] { + is_member = !is_member; + i += 1; + } + is_member + }) + .take(limit.get() + 1) + .collect::<KvResult<Vec<_>>>() + } else { + blocks_with_ud + .filter_ok(|bn| { + while i < is_member_changes.len() && *bn >= is_member_changes[i] { + is_member = !is_member; + i += 1; + } + is_member + }) + .collect::<KvResult<Vec<_>>>() + } + } else { + let is_member_changes: SmallVec<[BlockNumber; 4]> = + is_member_changes.into_iter().rev().collect(); + let mut i = 0; + let mut is_member = gva_idty.is_member; + if let Some(limit) = page_info.limit_opt { + blocks_with_ud + .filter_ok(|bn| { + /*println!( + "TMP (bn, is_member_changes[{}])=({}, {})", + i, bn, is_member_changes[i] + );*/ + while i < is_member_changes.len() && *bn < is_member_changes[i] { + is_member = !is_member; + i += 1; + } + is_member + }) + .take(limit.get()) + .collect::<KvResult<Vec<_>>>() + } else { + blocks_with_ud + .filter_ok(|bn| { + while i < is_member_changes.len() && *bn < is_member_changes[i] { + is_member = !is_member; + i += 1; + } + is_member + }) + .collect::<KvResult<Vec<_>>>() + } + } +} + +fn get_first_and_last_unspent_ud<BC: BackendCol>( + uds: &TxColRo<BC, UdsEvent>, + pubkey: PublicKey, + bn_to_exclude_opt: Option<&BTreeSet<BlockNumber>>, +) -> KvResult<(Option<BlockNumber>, Option<BlockNumber>)> { + if let Some(bn_to_exclude) = bn_to_exclude_opt { + Ok(( + uds.iter( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| { + it.keys() + .filter_map_ok(|UdIdV2(_p, bn)| { + if !bn_to_exclude.contains(&bn) { + Some(bn) + } else { + None + } + }) + .next_res() + }, + )?, + uds.iter_rev( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| { + it.keys() + .filter_map_ok(|UdIdV2(_p, bn)| { + if !bn_to_exclude.contains(&bn) { + Some(bn) + } else { + None + } + }) + .next_res() + }, + )?, + )) + } else { + Ok(( + uds.iter( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| it.keys().map_ok(|UdIdV2(_p, bn)| bn).next_res(), + )?, + uds.iter_rev( + UdIdV2(pubkey, BlockNumber(0))..UdIdV2(pubkey, BlockNumber(u32::MAX)), + |it| it.keys().map_ok(|UdIdV2(_p, bn)| bn).next_res(), + )?, + )) + } +} + +macro_rules! collect_one_ud { + ($block_number:ident, $current_ud:ident, $uds:ident, $sum:ident, $amount_target_opt:ident) => { + $uds.push(($block_number, $current_ud)); + $sum = $sum + $current_ud; + if let Some(amount_target) = $amount_target_opt { + if $sum >= amount_target { + return Ok(UdsWithSum { $uds, $sum }); + } + } + }; +} + +fn collect_uds<BC: BackendCol, I: Iterator<Item = BlockNumber>>( + mut blocks_numbers: I, + blocks_numbers_len: usize, + first_reval: U32BE, + uds_reval: TxColRo<BC, UdsRevalEvent>, + amount_opt: Option<SourceAmount>, +) -> KvResult<UdsWithSum> { + let uds_revals = uds_reval.iter(first_reval.., |it| it.collect::<KvResult<Vec<_>>>())?; + + if uds_revals.is_empty() { + Ok(UdsWithSum::default()) + } else { + let mut current_ud = (uds_revals[0].1).0; + let mut uds = Vec::with_capacity(blocks_numbers_len); + let mut sum = SourceAmount::ZERO; + + // Uds before last reval + for (block_reval, amount_reval) in &uds_revals[1..] { + 'blocks_numbers: while let Some(block_number) = blocks_numbers.next() { + if block_number.0 >= block_reval.0 { + current_ud = amount_reval.0; + collect_one_ud!(block_number, current_ud, uds, sum, amount_opt); + break 'blocks_numbers; + } else { + collect_one_ud!(block_number, current_ud, uds, sum, amount_opt); + } + } + } + + // Uds after last reval + for block_number in blocks_numbers { + collect_one_ud!(block_number, current_ud, uds, sum, amount_opt); + } + + Ok(UdsWithSum { uds, sum }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use duniter_core::dbs::smallvec::smallvec as svec; + use duniter_core::dbs::{databases::bc_v2::BcV2DbWritable, SourceAmountValV2, UdIdV2}; + use duniter_gva_db::GvaV1DbWritable; + + #[test] + fn test_filter_blocks_numbers() -> KvResult<()> { + let idty = GvaIdtyDbV1 { + is_member: true, + joins: svec![BlockNumber(26), BlockNumber(51)], + leaves: [BlockNumber(32)].iter().copied().collect(), + first_ud: Some(BlockNumber(29)), + }; + let blocks_with_ud = vec![ + BlockNumber(3), + BlockNumber(9), + BlockNumber(15), + BlockNumber(22), + BlockNumber(29), + BlockNumber(35), + BlockNumber(42), + BlockNumber(48), + BlockNumber(54), + BlockNumber(60), + ]; + + assert_eq!( + filter_blocks_numbers( + idty.clone(), + PageInfo { + pos: None, + order: true, + limit_opt: NonZeroUsize::new(1), + }, + blocks_with_ud.iter().copied().map(Ok), + )?, + vec![BlockNumber(29), BlockNumber(54)] + ); + assert_eq!( + filter_blocks_numbers( + idty, + PageInfo { + pos: None, + order: false, + limit_opt: None, + }, + blocks_with_ud.into_iter().rev().map(Ok), + )?, + vec![BlockNumber(60), BlockNumber(54), BlockNumber(29)] + ); + Ok(()) + } + + #[test] + fn test_all_uds_of_pubkey() -> KvResult<()> { + let pk = PublicKey::default(); + let idty = GvaIdtyDbV1 { + is_member: true, + joins: svec![BlockNumber(26), BlockNumber(51)], + leaves: [BlockNumber(32)].iter().copied().collect(), + first_ud: Some(BlockNumber(29)), + }; + + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + bc_db + .uds_reval_write() + .upsert(U32BE(0), SourceAmountValV2(SourceAmount::with_base0(10)))?; + bc_db + .uds_reval_write() + .upsert(U32BE(40), SourceAmountValV2(SourceAmount::with_base0(12)))?; + gva_db + .gva_identities_write() + .upsert(PubKeyKeyV2(pk), idty)?; + gva_db.blocks_with_ud_write().upsert(U32BE(22), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(29), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(35), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(42), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(48), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(54), ())?; + gva_db.blocks_with_ud_write().upsert(U32BE(60), ())?; + + // Get all uds + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey(&bc_db_ro, pk, PageInfo::default())?; + assert_eq!( + uds, + vec![ + (BlockNumber(29), SourceAmount::with_base0(10)), + (BlockNumber(54), SourceAmount::with_base0(12)), + (BlockNumber(60), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(34)); + assert!(!has_previous_page); + assert!(!has_next_page); + + // Get all uds with limit + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + limit_opt: NonZeroUsize::new(2), + ..Default::default() + }, + )?; + assert_eq!( + uds, + vec![ + (BlockNumber(29), SourceAmount::with_base0(10)), + (BlockNumber(54), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(22)); + assert!(!has_previous_page); + assert!(has_next_page); + + // Get all uds from particular position + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + pos: Some(BlockNumber(50)), + ..Default::default() + }, + )?; + assert_eq!( + uds, + vec![ + (BlockNumber(54), SourceAmount::with_base0(12)), + (BlockNumber(60), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(24)); + assert!(has_previous_page); + assert!(!has_next_page); + + // Get all uds on DESC order + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + order: false, + ..Default::default() + }, + )?; + assert_eq!( + uds, + vec![ + (BlockNumber(29), SourceAmount::with_base0(10)), + (BlockNumber(54), SourceAmount::with_base0(12)), + (BlockNumber(60), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(34)); + assert!(!has_previous_page); + assert!(!has_next_page); + + // Get all uds on DESC order with limit + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + order: false, + limit_opt: NonZeroUsize::new(2), + ..Default::default() + }, + )?; + assert_eq!( + uds, + vec![ + (BlockNumber(54), SourceAmount::with_base0(12)), + (BlockNumber(60), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(24)); + assert!(has_previous_page); + assert!(!has_next_page); + + // Get all uds on DESC order from particular position + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = db_reader.all_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + pos: Some(BlockNumber(55)), + order: false, + ..Default::default() + }, + )?; + assert_eq!( + uds, + vec![ + (BlockNumber(29), SourceAmount::with_base0(10)), + (BlockNumber(54), SourceAmount::with_base0(12)), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(22)); + assert!(!has_previous_page); + assert!(has_next_page); + + Ok(()) + } + + #[test] + fn test_unspent_uds_of_pubkey() -> KvResult<()> { + let pk = PublicKey::default(); + let bc_db = duniter_core::dbs::databases::bc_v2::BcV2Db::<Mem>::open(MemConf::default())?; + let bc_db_ro = bc_db.get_ro_handler(); + let dbs_reader = DbsReaderImpl::mem(); + + bc_db + .uds_reval_write() + .upsert(U32BE(0), SourceAmountValV2(SourceAmount::with_base0(10)))?; + bc_db + .uds_reval_write() + .upsert(U32BE(40), SourceAmountValV2(SourceAmount::with_base0(12)))?; + + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(0)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(10)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(20)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(30)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(40)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(50)), ())?; + bc_db.uds_write().upsert(UdIdV2(pk, BlockNumber(60)), ())?; + + // Get unspent uds + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = dbs_reader.unspent_uds_of_pubkey(&bc_db_ro, pk, PageInfo::default(), None, None)?; + assert_eq!(uds.len(), 7); + assert_eq!( + uds.first(), + Some(&(BlockNumber(0), SourceAmount::with_base0(10))) + ); + assert_eq!( + uds.last(), + Some(&(BlockNumber(60), SourceAmount::with_base0(12))) + ); + assert_eq!(sum, SourceAmount::with_base0(76)); + assert!(!has_previous_page); + assert!(!has_next_page); + + // Get unspent uds from particular position + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = dbs_reader.unspent_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + pos: Some(BlockNumber(30)), + ..Default::default() + }, + None, + None, + )?; + assert_eq!(uds.len(), 4); + assert_eq!( + uds.first(), + Some(&(BlockNumber(30), SourceAmount::with_base0(10))) + ); + assert_eq!( + uds.last(), + Some(&(BlockNumber(60), SourceAmount::with_base0(12))) + ); + assert_eq!(sum, SourceAmount::with_base0(46)); + assert!(has_previous_page); + assert!(!has_next_page); + + // Get unspent uds in order DESC + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = dbs_reader.unspent_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + order: false, + ..Default::default() + }, + None, + None, + )?; + assert_eq!(uds.len(), 7); + assert_eq!( + uds.first(), + Some(&(BlockNumber(0), SourceAmount::with_base0(10))) + ); + assert_eq!( + uds.last(), + Some(&(BlockNumber(60), SourceAmount::with_base0(12))) + ); + assert_eq!(sum, SourceAmount::with_base0(76)); + assert!(!has_previous_page); + assert!(!has_next_page); + + // Get unspent uds in order DESC from particular position + let PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + } = dbs_reader.unspent_uds_of_pubkey( + &bc_db_ro, + pk, + PageInfo { + pos: Some(BlockNumber(40)), + order: false, + ..Default::default() + }, + None, + None, + )?; + assert_eq!(uds.len(), 5); + assert_eq!( + uds.first(), + Some(&(BlockNumber(0), SourceAmount::with_base0(10))) + ); + assert_eq!( + uds.last(), + Some(&(BlockNumber(40), SourceAmount::with_base0(12))) + ); + assert_eq!(sum, SourceAmount::with_base0(52)); + assert!(!has_previous_page); + assert!(has_next_page); + + Ok(()) + } +} diff --git a/dbs-reader/src/utxos.rs b/dbs-reader/src/utxos.rs new file mode 100644 index 0000000000000000000000000000000000000000..5c9dc75cf9de5ae0fc0c58ef1151b84bf4d6487b --- /dev/null +++ b/dbs-reader/src/utxos.rs @@ -0,0 +1,500 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use dubp::documents::dubp_wallet::prelude::*; +use duniter_core::dbs::SourceAmountValV2; + +use crate::*; + +#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +pub struct UtxoCursor { + pub block_number: BlockNumber, + pub tx_hash: Hash, + pub output_index: u8, +} +impl std::fmt::Display for UtxoCursor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{}:{}", + self.block_number, self.tx_hash, self.output_index, + ) + } +} + +impl FromStr for UtxoCursor { + type Err = WrongCursor; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut s = s.split(':'); + let block_number = s + .next() + .ok_or(WrongCursor)? + .parse() + .map_err(|_| WrongCursor)?; + let tx_hash = Hash::from_hex(s.next().ok_or(WrongCursor)?).map_err(|_| WrongCursor)?; + let output_index = s + .next() + .ok_or(WrongCursor)? + .parse() + .map_err(|_| WrongCursor)?; + Ok(Self { + block_number, + tx_hash, + output_index, + }) + } +} + +#[derive(Debug, Default)] +pub struct UtxosWithSum { + pub utxos: Vec<(UtxoCursor, SourceAmount)>, + pub sum: SourceAmount, +} + +impl DbsReaderImpl { + pub(super) fn find_script_utxos_<TxsMpDb: 'static + TxsMpV2DbReadable>( + &self, + txs_mp_db_ro: &TxsMpDb, + amount_target_opt: Option<SourceAmount>, + page_info: PageInfo<UtxoCursor>, + script: &WalletScriptV10, + ) -> anyhow::Result<PagedData<UtxosWithSum>> { + let mempool_filter = |k_res: KvResult<GvaUtxoIdDbV1>| match k_res { + Ok(gva_utxo_id) => { + match txs_mp_db_ro.utxos_ids().contains_key(&UtxoIdDbV2( + gva_utxo_id.get_tx_hash(), + gva_utxo_id.get_output_index() as u32, + )) { + Ok(false) => Some(Ok(gva_utxo_id)), + Ok(true) => None, + Err(e) => Some(Err(e)), + } + } + Err(e) => Some(Err(e)), + }; + + let script_hash = Hash::compute(script.to_string().as_bytes()); + let (mut k_min, mut k_max) = GvaUtxoIdDbV1::script_interval(script_hash); + let first_cursor_opt = if page_info.not_all() { + self.0 + .gva_utxos() + .iter(k_min..k_max, |it| { + it.keys().filter_map(mempool_filter).next_res() + })? + .map(|gva_utxo_id| UtxoCursor { + block_number: BlockNumber(gva_utxo_id.get_block_number()), + tx_hash: gva_utxo_id.get_tx_hash(), + output_index: gva_utxo_id.get_output_index(), + }) + } else { + None + }; + let last_cursor_opt = if page_info.not_all() { + self.0 + .gva_utxos() + .iter_rev(k_min..k_max, |it| { + it.keys().filter_map(mempool_filter).next_res() + })? + .map(|gva_utxo_id| UtxoCursor { + block_number: BlockNumber(gva_utxo_id.get_block_number()), + tx_hash: gva_utxo_id.get_tx_hash(), + output_index: gva_utxo_id.get_output_index(), + }) + } else { + None + }; + if let Some(ref pos) = page_info.pos { + if page_info.order { + k_min = GvaUtxoIdDbV1::new_( + script_hash, + pos.block_number.0, + pos.tx_hash, + pos.output_index, + ); + } else { + k_max = GvaUtxoIdDbV1::new_( + script_hash, + pos.block_number.0, + pos.tx_hash, + pos.output_index, + ); + } + } + let UtxosWithSum { utxos, mut sum } = if page_info.order { + self.0.gva_utxos().iter(k_min..k_max, |it| { + find_script_utxos_inner(txs_mp_db_ro, amount_target_opt, page_info, it) + })? + } else { + self.0.gva_utxos().iter_rev(k_min..k_max, |it| { + find_script_utxos_inner(txs_mp_db_ro, amount_target_opt, page_info, it) + })? + }; + + if amount_target_opt.is_none() { + sum = utxos.iter().map(|(_utxo_id_with_bn, sa)| *sa).sum(); + } + + let order = page_info.order; + + Ok(PagedData { + has_next_page: has_next_page( + utxos + .iter() + .map(|(utxo_id_with_bn, _sa)| utxo_id_with_bn.into()), + last_cursor_opt, + page_info, + order, + ), + has_previous_page: has_previous_page( + utxos + .iter() + .map(|(utxo_id_with_bn, _sa)| utxo_id_with_bn.into()), + first_cursor_opt, + page_info, + order, + ), + data: UtxosWithSum { utxos, sum }, + }) + } + pub(super) fn first_scripts_utxos_( + &self, + amount_target_opt: Option<SourceAmount>, + first: usize, + scripts: &[WalletScriptV10], + ) -> anyhow::Result<Vec<ArrayVec<[Utxo; MAX_FIRST_UTXOS]>>> { + let iter = scripts.iter().map(|script| { + let (k_min, k_max) = + GvaUtxoIdDbV1::script_interval(Hash::compute(script.to_string().as_bytes())); + self.0.gva_utxos().iter(k_min..k_max, |it| { + it.take(first) + .map_ok(|(k, v)| Utxo { + amount: v.0, + tx_hash: k.get_tx_hash(), + output_index: k.get_output_index(), + }) + .collect::<KvResult<_>>() + }) + }); + if let Some(amount_target) = amount_target_opt { + let mut sum = SourceAmount::ZERO; + Ok(iter + .take_while(|utxos_res: &KvResult<ArrayVec<[Utxo; MAX_FIRST_UTXOS]>>| { + if let Ok(utxos) = utxos_res { + sum = sum + utxos.iter().map(|utxo| utxo.amount).sum(); + sum <= amount_target + } else { + true + } + }) + .collect::<KvResult<Vec<_>>>()?) + } else { + Ok(iter.collect::<KvResult<Vec<_>>>()?) + } + } +} + +fn find_script_utxos_inner<TxsMpDb, I>( + txs_mp_db_ro: &TxsMpDb, + amount_target_opt: Option<SourceAmount>, + page_info: PageInfo<UtxoCursor>, + utxos_iter: I, +) -> KvResult<UtxosWithSum> +where + TxsMpDb: TxsMpV2DbReadable, + I: Iterator<Item = KvResult<(GvaUtxoIdDbV1, SourceAmountValV2)>>, +{ + let mut sum = SourceAmount::ZERO; + + let it = utxos_iter.filter_map(|entry_res| match entry_res { + Ok((gva_utxo_id, SourceAmountValV2(utxo_amount))) => { + if utxo_amount.amount() < super::find_inputs::MIN_AMOUNT { + None + } else { + let tx_hash = gva_utxo_id.get_tx_hash(); + let output_index = gva_utxo_id.get_output_index(); + match txs_mp_db_ro + .utxos_ids() + .contains_key(&UtxoIdDbV2(tx_hash, output_index as u32)) + { + Ok(false) => Some(Ok(( + UtxoCursor { + tx_hash, + output_index, + block_number: BlockNumber(gva_utxo_id.get_block_number()), + }, + utxo_amount, + ))), + Ok(true) => None, + Err(e) => Some(Err(e)), + } + } + } + Err(e) => Some(Err(e)), + }); + let utxos = if let Some(limit) = page_info.limit_opt { + if let Some(total_target) = amount_target_opt { + it.take(limit.get()) + .take_while(|res| match res { + Ok((_, utxo_amount)) => { + if sum < total_target { + sum = sum + *utxo_amount; + true + } else { + false + } + } + Err(_) => true, + }) + .collect::<KvResult<Vec<_>>>()? + } else { + it.take(limit.get()).collect::<KvResult<Vec<_>>>()? + } + } else if let Some(total_target) = amount_target_opt { + it.take_while(|res| match res { + Ok((_, utxo_amount)) => { + if sum < total_target { + sum = sum + *utxo_amount; + true + } else { + false + } + } + Err(_) => true, + }) + .collect::<KvResult<Vec<_>>>()? + } else { + it.collect::<KvResult<Vec<_>>>()? + }; + + Ok(UtxosWithSum { utxos, sum }) +} + +#[cfg(test)] +mod tests { + + use super::*; + use dubp::crypto::keys::PublicKey as _; + use duniter_core::dbs::databases::txs_mp_v2::TxsMpV2DbWritable; + use duniter_gva_db::GvaV1DbWritable; + use unwrap::unwrap; + + #[test] + fn test_first_scripts_utxos() -> anyhow::Result<()> { + let script = WalletScriptV10::single_sig(PublicKey::default()); + let script2 = WalletScriptV10::single_sig(unwrap!(PublicKey::from_base58( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + ))); + + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(500)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 1, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(800)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 2, Hash::default(), 2), + SourceAmountValV2(SourceAmount::with_base0(1_200)), + )?; + + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script2.clone(), 0, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(400)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script2.clone(), 1, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(700)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script2.clone(), 2, Hash::default(), 2), + SourceAmountValV2(SourceAmount::with_base0(1_100)), + )?; + + assert_eq!( + db_reader.first_scripts_utxos(None, 2, &[script, script2])?, + vec![ + [ + Utxo { + amount: SourceAmount::with_base0(500), + tx_hash: Hash::default(), + output_index: 0, + }, + Utxo { + amount: SourceAmount::with_base0(800), + tx_hash: Hash::default(), + output_index: 1, + }, + ] + .iter() + .copied() + .collect::<ArrayVec<_>>(), + [ + Utxo { + amount: SourceAmount::with_base0(400), + tx_hash: Hash::default(), + output_index: 0, + }, + Utxo { + amount: SourceAmount::with_base0(700), + tx_hash: Hash::default(), + output_index: 1, + }, + ] + .iter() + .copied() + .collect::<ArrayVec<_>>() + ] + ); + + Ok(()) + } + + #[test] + fn test_find_script_utxos() -> anyhow::Result<()> { + let script = WalletScriptV10::single_sig(PublicKey::default()); + + let gva_db = duniter_gva_db::GvaV1Db::<Mem>::open(MemConf::default())?; + let db_reader = create_dbs_reader(unsafe { std::mem::transmute(&gva_db.get_ro_handler()) }); + let txs_mp_db = + duniter_core::dbs::databases::txs_mp_v2::TxsMpV2Db::<Mem>::open(MemConf::default())?; + + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(500)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(800)), + )?; + gva_db.gva_utxos_write().upsert( + GvaUtxoIdDbV1::new(script.clone(), 0, Hash::default(), 2), + SourceAmountValV2(SourceAmount::with_base0(1200)), + )?; + + // Find utxos with amount target + let PagedData { + data: UtxosWithSum { utxos, sum }, + has_next_page, + has_previous_page, + } = db_reader.find_script_utxos( + &txs_mp_db, + Some(SourceAmount::with_base0(550)), + PageInfo::default(), + &script, + )?; + + assert_eq!( + utxos, + vec![ + ( + UtxoCursor { + block_number: BlockNumber(0), + tx_hash: Hash::default(), + output_index: 0, + }, + SourceAmount::with_base0(500) + ), + ( + UtxoCursor { + block_number: BlockNumber(0), + tx_hash: Hash::default(), + output_index: 1, + }, + SourceAmount::with_base0(800) + ), + ] + ); + assert_eq!(sum, SourceAmount::with_base0(1300)); + assert!(!has_next_page); + assert!(!has_previous_page); + + // Find utxos with amount target in DESC order + let PagedData { + data: UtxosWithSum { utxos, sum }, + .. + } = db_reader.find_script_utxos( + &txs_mp_db, + Some(SourceAmount::with_base0(550)), + PageInfo { + order: false, + ..Default::default() + }, + &script, + )?; + + assert_eq!( + utxos, + vec![( + UtxoCursor { + block_number: BlockNumber(0), + tx_hash: Hash::default(), + output_index: 2, + }, + SourceAmount::with_base0(1200) + ),] + ); + assert_eq!(sum, SourceAmount::with_base0(1200)); + assert!(!has_next_page); + assert!(!has_previous_page); + + // Find utxos with limit in DESC order + let PagedData { + data: UtxosWithSum { utxos, sum }, + has_previous_page, + has_next_page, + } = db_reader.find_script_utxos( + &txs_mp_db, + None, + PageInfo { + order: false, + limit_opt: NonZeroUsize::new(2), + ..Default::default() + }, + &script, + )?; + + assert_eq!( + utxos, + vec![ + ( + UtxoCursor { + block_number: BlockNumber(0), + tx_hash: Hash::default(), + output_index: 2, + }, + SourceAmount::with_base0(1200) + ), + ( + UtxoCursor { + block_number: BlockNumber(0), + tx_hash: Hash::default(), + output_index: 1, + }, + SourceAmount::with_base0(800) + ) + ] + ); + assert_eq!(sum, SourceAmount::with_base0(2000)); + assert!(!has_next_page); + assert!(has_previous_page); + + Ok(()) + } +} diff --git a/gql/Cargo.toml b/gql/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e78f22ef8fc18f57f4f372adbac0628a1551f406 --- /dev/null +++ b/gql/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "duniter-gva-gql" +version = "0.1.0" +authors = ["librelois <elois@duniter.org>"] +license = "AGPL-3.0" +edition = "2018" + +[dependencies] +anyhow = "1.0.33" +arrayvec = "0.5.1" +async-graphql = { version = "2.8", features = ["log"] } +async-trait = "0.1.41" +dubp = { version = "0.51.0", features = ["duniter"] } +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +duniter-gva-db = { path = "../db" } +duniter-gva-dbs-reader = { path = "../dbs-reader" } +fast-threadpool = "0.2.3" +flume = "0.10.0" +futures = "0.3.6" +log = "0.4.11" +resiter = "0.4.0" +serde = { version = "1.0.105", features = ["derive"] } + +[dev-dependencies] +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core", features = ["mem", "mock"] } +duniter-gva-dbs-reader = { path = "../dbs-reader", features = ["mock"] } +mockall = "0.9.1" +pretty_assertions = "0.7" +serde_json = "1.0.53" +tokio = { version = "1.2", features = ["macros", "rt-multi-thread", "time"] } +unwrap = "1.2.1" diff --git a/gql/src/entities.rs b/gql/src/entities.rs new file mode 100644 index 0000000000000000000000000000000000000000..c6ac1569f842a1cf73575aaf264dc65ca57e7a51 --- /dev/null +++ b/gql/src/entities.rs @@ -0,0 +1,112 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod block_gva; +pub mod idty_gva; +pub mod network; +pub mod tx_gva; +pub mod ud_gva; +pub mod utxos_gva; + +use crate::*; + +#[derive(Default, async_graphql::SimpleObject)] +pub(crate) struct AggregateSum { + pub(crate) aggregate: Sum, +} + +#[derive(Default, async_graphql::SimpleObject)] +pub(crate) struct AmountWithBase { + pub(crate) amount: i32, + pub(crate) base: i32, +} + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct EdgeTx { + pub(crate) direction: TxDirection, +} + +pub(crate) enum RawTxOrChanges { + FinalTx(String), + Changes(Vec<String>), +} +#[async_graphql::Object] +impl RawTxOrChanges { + /// Intermediate transactions documents for compacting sources (`null` if not needed) + async fn changes(&self) -> Option<&Vec<String>> { + if let Self::Changes(changes) = self { + Some(changes) + } else { + None + } + } + /// Transaction document that carries out the requested transfer (`null` if the amount to be sent requires too many sources) + async fn tx(&self) -> Option<&str> { + if let Self::FinalTx(raw_tx) = self { + Some(raw_tx.as_str()) + } else { + None + } + } +} + +#[derive(Default, async_graphql::SimpleObject)] +pub(crate) struct Sum { + pub(crate) sum: AmountWithBase, +} + +#[derive(Clone, Copy, Eq, PartialEq, async_graphql::Enum)] +pub(crate) enum TxDirection { + /// Received + Received, + /// Sent + Sent, +} + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct TxsHistoryMempool { + /// Transactions sending + pub(crate) sending: Vec<TxGva>, + /// Transactions receiving + pub(crate) receiving: Vec<TxGva>, +} + +#[derive(Clone, async_graphql::SimpleObject)] +pub(crate) struct UtxoGva { + /// Source amount + pub(crate) amount: i64, + /// Source base + pub(crate) base: i64, + /// Hash of origin transaction + pub(crate) tx_hash: String, + /// Index of output in origin transaction + pub(crate) output_index: u32, +} + +#[derive(Clone, async_graphql::SimpleObject)] +pub(crate) struct UtxoTimedGva { + /// Source amount + pub(crate) amount: i64, + /// Source base + pub(crate) base: i64, + /// Hash of origin transaction + pub(crate) tx_hash: String, + /// Index of output in origin transaction + pub(crate) output_index: u32, + /// Written block + pub(crate) written_block: u32, + /// Written time + pub(crate) written_time: u64, +} diff --git a/gql/src/entities/block_gva.rs b/gql/src/entities/block_gva.rs new file mode 100644 index 0000000000000000000000000000000000000000..c12b8c2f15df10f4cf48befcd83f87099c6a1566 --- /dev/null +++ b/gql/src/entities/block_gva.rs @@ -0,0 +1,176 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use super::tx_gva::TxGva; +use crate::*; +use dubp::block::DubpBlockV10; +use duniter_core::dbs::BlockMetaV2; + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct BlockMeta { + pub version: u64, + pub number: u32, + pub hash: String, + pub signature: String, + pub inner_hash: String, + pub previous_hash: String, + pub issuer: String, + pub time: u64, + pub pow_min: u32, + pub members_count: u64, + pub issuers_count: u32, + pub issuers_frame: u64, + pub median_time: u64, + pub nonce: u64, + pub monetary_mass: u64, + pub unit_base: u32, + pub dividend: Option<u32>, +} + +impl From<BlockMetaV2> for BlockMeta { + fn from(block_db: BlockMetaV2) -> Self { + Self::from(&block_db) + } +} +impl From<&BlockMetaV2> for BlockMeta { + fn from(block_db: &BlockMetaV2) -> Self { + BlockMeta { + version: block_db.version, + number: block_db.number, + hash: block_db.hash.to_string(), + signature: block_db.signature.to_string(), + inner_hash: block_db.inner_hash.to_string(), + previous_hash: block_db.previous_hash.to_string(), + issuer: block_db.issuer.to_string(), + time: block_db.time, + pow_min: block_db.pow_min, + members_count: block_db.members_count, + issuers_count: block_db.issuers_count, + issuers_frame: block_db.issuers_frame, + median_time: block_db.median_time, + nonce: block_db.nonce, + monetary_mass: block_db.monetary_mass, + unit_base: block_db.unit_base, + dividend: block_db.dividend.map(|sa| sa.amount() as u32), + } + } +} + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct Block { + // Meta + pub version: u64, + pub number: u32, + pub hash: String, + pub signature: String, + pub inner_hash: String, + pub previous_hash: Option<String>, + pub issuer: String, + pub time: u64, + pub pow_min: u32, + pub members_count: u64, + pub issuers_count: u32, + pub issuers_frame: u64, + pub median_time: u64, + pub nonce: u64, + pub monetary_mass: u64, + pub unit_base: u32, + pub dividend: Option<u32>, + // Payload + /// Identities + pub identities: Vec<String>, + /// joiners + pub joiners: Vec<String>, + /// Actives (=renewals) + pub actives: Vec<String>, + /// Leavers + pub leavers: Vec<String>, + /// Revokeds + pub revoked: Vec<String>, + /// Excludeds + pub excluded: Vec<String>, + /// Certifications + pub certifications: Vec<String>, + pub transactions: Vec<TxGva>, +} + +impl From<&DubpBlockV10> for Block { + fn from(block: &DubpBlockV10) -> Self { + let block = block.to_string_object(); + Block { + // Meta + version: block.version, + number: block.number as u32, + hash: block.hash.unwrap_or_default(), + signature: block.signature, + inner_hash: block.inner_hash.unwrap_or_default(), + previous_hash: block.previous_hash, + issuer: block.issuer, + time: block.time, + pow_min: block.pow_min as u32, + members_count: block.members_count, + issuers_count: block.issuers_count as u32, + issuers_frame: block.issuers_frame, + median_time: block.median_time, + nonce: block.nonce, + monetary_mass: block.monetary_mass, + unit_base: block.unit_base as u32, + dividend: block.dividend.map(|amount| amount as u32), + // Payload + identities: block.identities, + joiners: block.joiners, + actives: block.actives, + leavers: block.leavers, + revoked: block.revoked, + excluded: block.excluded, + certifications: block.certifications, + transactions: block.transactions.into_iter().map(Into::into).collect(), + } + } +} + +impl From<&BlockMetaV2> for Block { + fn from(block: &BlockMetaV2) -> Self { + Block { + // Meta + version: block.version, + number: block.number, + hash: block.hash.to_string(), + signature: block.signature.to_string(), + inner_hash: block.inner_hash.to_string(), + previous_hash: Some(block.previous_hash.to_string()), + issuer: block.issuer.to_string(), + time: block.time, + pow_min: block.pow_min, + members_count: block.members_count, + issuers_count: block.issuers_count, + issuers_frame: block.issuers_frame, + median_time: block.median_time, + nonce: block.nonce, + monetary_mass: block.monetary_mass, + unit_base: block.unit_base, + dividend: block.dividend.map(|sa| sa.amount() as u32), + // Payload + identities: vec![], + joiners: vec![], + actives: vec![], + leavers: vec![], + revoked: vec![], + excluded: vec![], + certifications: vec![], + transactions: vec![], + } + } +} diff --git a/gql/src/entities/idty_gva.rs b/gql/src/entities/idty_gva.rs new file mode 100644 index 0000000000000000000000000000000000000000..ee77b0e4db267306971fa64f4e28f460d42f1b2c --- /dev/null +++ b/gql/src/entities/idty_gva.rs @@ -0,0 +1,20 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct Identity { + pub is_member: bool, + pub username: String, +} diff --git a/gql/src/entities/network.rs b/gql/src/entities/network.rs new file mode 100644 index 0000000000000000000000000000000000000000..4e065909c8cb5431c1b9858fbfed4780ec5478b1 --- /dev/null +++ b/gql/src/entities/network.rs @@ -0,0 +1,74 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#[derive(Default, async_graphql::SimpleObject)] +#[graphql(name = "Peer")] +pub struct PeerCardGva { + pub version: u32, + pub currency: String, + pub pubkey: String, + pub blockstamp: String, + pub endpoints: Vec<String>, + pub status: String, + pub signature: String, +} +impl From<duniter_core::dbs::PeerCardDbV1> for PeerCardGva { + fn from(peer: duniter_core::dbs::PeerCardDbV1) -> Self { + Self { + version: peer.version, + currency: peer.currency, + pubkey: peer.pubkey, + blockstamp: peer.blockstamp, + endpoints: peer.endpoints, + status: peer.status, + signature: peer.signature, + } + } +} + +#[derive(Default, async_graphql::SimpleObject)] +#[graphql(name = "Head")] +pub struct HeadGva { + pub api: String, + pub pubkey: String, + pub blockstamp: String, + pub software: String, + pub software_version: String, + pub pow_prefix: u32, + pub free_member_room: u32, + pub free_mirror_room: u32, + pub signature: String, +} +impl From<duniter_core::dbs::DunpHeadDbV1> for HeadGva { + fn from(head: duniter_core::dbs::DunpHeadDbV1) -> Self { + Self { + api: head.api, + pubkey: head.pubkey.to_string(), + blockstamp: head.blockstamp.to_string(), + software: head.software, + software_version: head.software_version, + pow_prefix: head.pow_prefix, + free_member_room: head.free_member_room, + free_mirror_room: head.free_member_room, + signature: head.signature.to_string(), + } + } +} + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct PeerWithHeads { + pub peer: PeerCardGva, + pub heads: Vec<HeadGva>, +} diff --git a/gql/src/entities/tx_gva.rs b/gql/src/entities/tx_gva.rs new file mode 100644 index 0000000000000000000000000000000000000000..6fd3764338d17b728705c301a3383c08f243b6c1 --- /dev/null +++ b/gql/src/entities/tx_gva.rs @@ -0,0 +1,84 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::documents::transaction::TransactionDocumentV10Stringified; +use duniter_gva_db::GvaTxDbV1; + +#[derive(async_graphql::SimpleObject)] +pub(crate) struct TxGva { + /// Version. + pub version: i32, + /// Currency. + pub currency: String, + /// Blockstamp + pub blockstamp: String, + /// Locktime + pub locktime: u64, + /// Document issuers. + pub issuers: Vec<String>, + /// Transaction inputs. + pub inputs: Vec<String>, + /// Inputs unlocks. + pub unlocks: Vec<String>, + /// Transaction outputs. + pub outputs: Vec<String>, + /// Transaction comment + pub comment: String, + /// Document signatures + pub signatures: Vec<String>, + /// Transaction hash + pub hash: String, + /// Written block + pub written_block: Option<String>, + /// Written Time + pub written_time: Option<i64>, +} + +impl From<GvaTxDbV1> for TxGva { + fn from(db_tx: GvaTxDbV1) -> Self { + let mut self_: TxGva = (&db_tx.tx).into(); + self_.written_block = Some(db_tx.written_block.to_string()); + self_.written_time = Some(db_tx.written_time); + self_ + } +} + +impl From<&TransactionDocumentV10> for TxGva { + fn from(tx: &TransactionDocumentV10) -> Self { + let tx_stringified = tx.to_string_object(); + Self::from(tx_stringified) + } +} + +impl From<TransactionDocumentV10Stringified> for TxGva { + fn from(tx_stringified: TransactionDocumentV10Stringified) -> Self { + Self { + version: 10, + currency: tx_stringified.currency, + blockstamp: tx_stringified.blockstamp, + locktime: tx_stringified.locktime, + issuers: tx_stringified.issuers, + inputs: tx_stringified.inputs, + unlocks: tx_stringified.unlocks, + outputs: tx_stringified.outputs, + comment: tx_stringified.comment, + signatures: tx_stringified.signatures, + hash: tx_stringified.hash.unwrap_or_default(), + written_block: None, + written_time: None, + } + } +} diff --git a/gql/src/entities/ud_gva.rs b/gql/src/entities/ud_gva.rs new file mode 100644 index 0000000000000000000000000000000000000000..e268f5b67803645fd602666938ce14d00d8618cd --- /dev/null +++ b/gql/src/entities/ud_gva.rs @@ -0,0 +1,48 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, async_graphql::SimpleObject)] +pub(crate) struct CurrentUdGva { + /// Ud amount + pub(crate) amount: i64, + /// Ud base + pub(crate) base: i64, +} + +#[derive(Clone, async_graphql::SimpleObject)] +pub(crate) struct RevalUdGva { + /// Ud amount + pub(crate) amount: i64, + /// Ud base + pub(crate) base: i64, + /// Number of the block that revaluate ud amount + pub(crate) block_number: u32, +} + +#[derive(Clone, async_graphql::SimpleObject)] +pub(crate) struct UdGva { + /// Ud amount + pub(crate) amount: i64, + /// Ud base + pub(crate) base: i64, + /// Issuer of this universal dividend + pub(crate) issuer: PubKeyGva, + /// Number of the block that created this UD + pub(crate) block_number: u32, + /// Blockchain time of the block that created this UD + pub(crate) blockchain_time: u64, +} diff --git a/gql/src/entities/utxos_gva.rs b/gql/src/entities/utxos_gva.rs new file mode 100644 index 0000000000000000000000000000000000000000..9bd3e79906e1e8b1289e62f71d85dc3b6aeacadd --- /dev/null +++ b/gql/src/entities/utxos_gva.rs @@ -0,0 +1,42 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(crate) struct UtxosGva(pub arrayvec::ArrayVec<[UtxoGva; 40]>); +impl async_graphql::Type for UtxosGva { + fn type_name() -> Cow<'static, str> { + Cow::Owned(format!("[{}]", UtxoGva::qualified_type_name())) + } + + fn qualified_type_name() -> String { + format!("[{}]!", UtxoGva::qualified_type_name()) + } + + fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String { + UtxoGva::create_type_info(registry); + Self::qualified_type_name() + } +} +#[async_trait::async_trait] +impl async_graphql::OutputType for UtxosGva { + async fn resolve( + &self, + ctx: &async_graphql::ContextSelectionSet<'_>, + field: &async_graphql::Positioned<async_graphql::parser::types::Field>, + ) -> async_graphql::ServerResult<async_graphql::Value> { + async_graphql::resolver_utils::resolve_list(ctx, field, &self.0, Some(self.0.len())).await + } +} diff --git a/gql/src/inputs.rs b/gql/src/inputs.rs new file mode 100644 index 0000000000000000000000000000000000000000..b904690941b7a6c4deac1cd019404f9cf3de96f6 --- /dev/null +++ b/gql/src/inputs.rs @@ -0,0 +1,57 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(async_graphql::InputObject, Clone, Copy, Default)] +pub(crate) struct TimeInterval { + pub(crate) from: Option<u64>, + pub(crate) to: Option<u64>, +} + +#[derive(async_graphql::InputObject)] +pub(crate) struct TxIssuer { + /// Account script (default is a script needed all provided signers) + pub(crate) script: Option<String>, + /// Signers + #[graphql(validator(ListMinLength(length = "1")))] + pub(crate) signers: Vec<String>, + /// XHX codes needed to unlock funds + #[graphql(validator(ListMinLength(length = "1")))] + pub(crate) codes: Option<Vec<String>>, + /// Amount + #[graphql(validator(IntGreaterThan(value = "0")))] + pub(crate) amount: i32, +} + +#[derive(async_graphql::InputObject)] +pub(crate) struct TxRecipient { + /// Amount + #[graphql(validator(IntGreaterThan(value = "0")))] + pub(crate) amount: i32, + /// Account script + pub(crate) script: String, +} + +#[derive(Clone, Copy, async_graphql::Enum, Eq, PartialEq)] +pub(crate) enum UdsFilter { + All, + Unspent, +} +impl Default for UdsFilter { + fn default() -> Self { + UdsFilter::All + } +} diff --git a/gql/src/inputs_validators.rs b/gql/src/inputs_validators.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2578c22bb8eceb0cea3726729baee9fd66eaba0 --- /dev/null +++ b/gql/src/inputs_validators.rs @@ -0,0 +1,36 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(crate) struct TxCommentValidator; + +impl async_graphql::validators::InputValueValidator for TxCommentValidator { + fn is_valid(&self, value: &async_graphql::Value) -> Result<(), String> { + if let async_graphql::Value::String(comment) = value { + if !TransactionDocumentV10::verify_comment(&comment) { + // Validation failed + Err("invalid comment".to_owned()) + } else { + // Validation succeeded + Ok(()) + } + } else { + // If the type does not match we can return None and built-in validations + // will pick up on the error + Ok(()) + } + } +} diff --git a/gql/src/lib.rs b/gql/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..64a48b51ccaab5abd1e74dd8549e6b616ac341b2 --- /dev/null +++ b/gql/src/lib.rs @@ -0,0 +1,157 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +mod entities; +mod inputs; +mod inputs_validators; +mod mutations; +mod pagination; +mod queries; +mod scalars; +mod schema; +mod subscriptions; + +pub use schema::{build_schema_with_data, get_schema_definition, GvaSchema, GvaSchemaData}; + +use crate::entities::{ + block_gva::{Block, BlockMeta}, + idty_gva::Identity, + network::{HeadGva, PeerCardGva, PeerWithHeads}, + tx_gva::TxGva, + ud_gva::{CurrentUdGva, RevalUdGva, UdGva}, + utxos_gva::UtxosGva, + AggregateSum, AmountWithBase, EdgeTx, RawTxOrChanges, Sum, TxDirection, TxsHistoryMempool, + UtxoGva, UtxoTimedGva, +}; +use crate::inputs::{TxIssuer, TxRecipient, UdsFilter}; +use crate::inputs_validators::TxCommentValidator; +use crate::pagination::Pagination; +use crate::scalars::{PkOrScriptGva, PubKeyGva}; +#[cfg(test)] +use crate::tests::AsyncAccessor; +#[cfg(test)] +use crate::tests::DbsReaderImpl; +use async_graphql::connection::{Connection, Edge, EmptyFields}; +use async_graphql::validators::{IntGreaterThan, IntRange, ListMaxLength, ListMinLength}; +use dubp::common::crypto::keys::{ed25519::PublicKey, PublicKey as _}; +use dubp::common::prelude::*; +use dubp::crypto::hashs::Hash; +use dubp::documents::prelude::*; +use dubp::documents::transaction::{TransactionDocumentTrait, TransactionDocumentV10}; +use dubp::documents_parser::prelude::*; +use dubp::wallet::prelude::*; +use duniter_core::dbs::databases::txs_mp_v2::TxsMpV2DbReadable; +use duniter_core::dbs::prelude::*; +use duniter_core::dbs::{kv_typed::prelude::*, FileBackend}; +#[cfg(not(test))] +use duniter_core::global::AsyncAccessor; +use duniter_core::mempools::TxsMempool; +use duniter_gva_dbs_reader::pagination::PageInfo; +use duniter_gva_dbs_reader::DbsReader; +#[cfg(not(test))] +use duniter_gva_dbs_reader::DbsReaderImpl; +use futures::{Stream, StreamExt}; +use resiter::map::Map; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroUsize, ops::Deref}; + +#[derive(Clone, Copy, Debug, Default)] +pub struct QueryContext { + pub is_whitelisted: bool, +} + +#[derive(Debug, Default)] +pub struct ServerMetaData { + pub currency: String, + pub self_pubkey: PublicKey, + pub software_version: &'static str, +} + +#[cfg(test)] +mod tests { + pub use duniter_core::global::{CurrentMeta, MockAsyncAccessor}; + pub use duniter_gva_dbs_reader::MockDbsReader; + + use super::*; + use fast_threadpool::ThreadPoolConfig; + + pub type AsyncAccessor = duniter_core::dbs::kv_typed::prelude::Arc<MockAsyncAccessor>; + pub type DbsReaderImpl = duniter_core::dbs::kv_typed::prelude::Arc<MockDbsReader>; + + pub(crate) fn create_schema( + mock_cm: MockAsyncAccessor, + dbs_ops: MockDbsReader, + ) -> KvResult<GvaSchema> { + let dbs = SharedDbs::mem()?; + let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs); + Ok(schema::build_schema_with_data( + schema::GvaSchemaData { + cm_accessor: Arc::new(mock_cm), + dbs_pool: threadpool.into_async_handler(), + dbs_reader: Arc::new(dbs_ops), + server_meta_data: ServerMetaData { + currency: "test_currency".to_owned(), + self_pubkey: PublicKey::default(), + software_version: "test", + }, + txs_mempool: TxsMempool::new(10), + }, + true, + )) + } + + pub(crate) async fn exec_graphql_request( + schema: &GvaSchema, + request: &str, + ) -> anyhow::Result<serde_json::Value> { + Ok(serde_json::to_value( + schema + .execute(async_graphql::Request::new(request).data(QueryContext::default())) + .await, + )?) + } + + /*pub(crate) fn create_schema_sub(dbs: SharedDbs<FileBackend>) -> KvResult<GvaSchema> { + let threadpool = fast_threadpool::ThreadPool::start(ThreadPoolConfig::default(), dbs); + Ok(schema::build_schema_with_data( + schema::GvaSchemaData { + dbs_pool: threadpool.into_async_handler(), + dbs_reader: Arc::new(MockDbsReader::new()), + server_meta_data: ServerMetaData { + currency: "test_currency".to_owned(), + self_pubkey: PublicKey::default(), + software_version: "test", + }, + txs_mempool: TxsMempool::new(10), + }, + true, + )) + } + + pub(crate) fn exec_graphql_subscription( + schema: &GvaSchema, + request: String, + ) -> impl Stream<Item = serde_json::Result<serde_json::Value>> + Send { + schema.execute_stream(request).map(serde_json::to_value) + }*/ +} diff --git a/gql/src/mutations.rs b/gql/src/mutations.rs new file mode 100644 index 0000000000000000000000000000000000000000..3d258e9ae7c83142ab3d4d166cf647590d59ce38 --- /dev/null +++ b/gql/src/mutations.rs @@ -0,0 +1,86 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Clone, Copy, Default)] +pub struct MutationRoot; + +#[async_graphql::Object] +impl MutationRoot { + /// Process a transaction + /// Return the transaction if it successfully inserted + async fn tx( + &self, + ctx: &async_graphql::Context<'_>, + raw_tx: String, + ) -> async_graphql::Result<TxGva> { + let tx = TransactionDocumentV10::parse_from_raw_text(&raw_tx)?; + + let data = ctx.data::<GvaSchemaData>()?; + let expected_currency = data.server_meta_data.currency.clone(); + + tx.verify(Some(&expected_currency))?; + + let server_pubkey = data.server_meta_data.self_pubkey; + let txs_mempool = data.txs_mempool; + + let tx = data + .dbs_pool + .execute(move |dbs| { + txs_mempool + .add_pending_tx(&dbs.bc_db_ro, server_pubkey, &dbs.txs_mp_db, &tx) + .map(|()| tx) + }) + .await??; + + Ok(TxGva::from(&tx)) + } + + /// Process several transactions + /// Return an array of successfully inserted transactions + async fn txs( + &self, + ctx: &async_graphql::Context<'_>, + raw_txs: Vec<String>, + ) -> async_graphql::Result<Vec<TxGva>> { + let txs = raw_txs + .iter() + .map(|raw_tx| TransactionDocumentV10::parse_from_raw_text(&raw_tx)) + .collect::<Result<Vec<TransactionDocumentV10>, _>>()?; + + let data = ctx.data::<GvaSchemaData>()?; + let expected_currency = data.server_meta_data.currency.clone(); + + let server_pubkey = data.server_meta_data.self_pubkey; + let txs_mempool = data.txs_mempool; + + let mut processed_txs = Vec::with_capacity(txs.len()); + for tx in txs { + tx.verify(Some(&expected_currency))?; + let tx = data + .dbs_pool + .execute(move |dbs| { + txs_mempool + .add_pending_tx(&dbs.bc_db_ro, server_pubkey, &dbs.txs_mp_db, &tx) + .map(|()| tx) + }) + .await??; + processed_txs.push(TxGva::from(&tx)); + } + + Ok(processed_txs) + } +} diff --git a/gql/src/pagination.rs b/gql/src/pagination.rs new file mode 100644 index 0000000000000000000000000000000000000000..98db4e9ab7c1644168deaedd7f37ca001f099e15 --- /dev/null +++ b/gql/src/pagination.rs @@ -0,0 +1,73 @@ +use std::str::FromStr; + +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +const MAX_PAGE_SIZE: u32 = 1_000; + +#[derive(Clone, Copy, async_graphql::Enum, Eq, PartialEq)] +pub(crate) enum Order { + /// Ascending order + Asc, + /// Decreasing order + Desc, +} +impl Default for Order { + fn default() -> Self { + Order::Asc + } +} + +#[derive(async_graphql::InputObject)] +pub(crate) struct Pagination { + /// Identifier of the 1st desired element (of the last one in descending order) + cursor: Option<String>, + ord: Order, + page_size: u32, +} + +impl Default for Pagination { + fn default() -> Self { + Pagination { + cursor: None, + ord: Order::default(), + page_size: 10, + } + } +} + +impl Pagination { + pub(crate) fn convert_to_page_info< + E: 'static + std::error::Error + Send + Sync, + T: FromStr<Err = E>, + >( + self, + is_whitelisted: bool, + ) -> anyhow::Result<duniter_gva_dbs_reader::PageInfo<T>> { + let page_size = if is_whitelisted || (self.page_size > 0 && self.page_size < MAX_PAGE_SIZE) + { + NonZeroUsize::new(self.page_size as usize) + } else { + return Err(anyhow::Error::msg("pageSize must be between 1 and 1000.")); + }; + Ok(duniter_gva_dbs_reader::PageInfo::new( + self.cursor.map(|c| T::from_str(&c)).transpose()?, + self.ord == Order::Asc, + page_size, + )) + } +} diff --git a/gql/src/queries.rs b/gql/src/queries.rs new file mode 100644 index 0000000000000000000000000000000000000000..ad733e3a557962eb6fa5217c523d734e45266b36 --- /dev/null +++ b/gql/src/queries.rs @@ -0,0 +1,86 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod account_balance; +pub mod block; +pub mod current_block; +pub mod current_frame; +pub mod first_utxos_of_scripts; +pub mod gen_tx; +pub mod idty; +pub mod network; +pub mod txs_history; +pub mod uds; +pub mod utxos_of_script; + +use crate::*; + +#[derive(async_graphql::MergedObject, Default)] +pub struct QueryRoot( + queries::NodeQuery, + queries::account_balance::AccountBalanceQuery, + queries::block::BlockQuery, + queries::current_block::CurrentBlockQuery, + queries::current_frame::CurrentFrameQuery, + queries::first_utxos_of_scripts::FirstUtxosQuery, + queries::gen_tx::GenTxsQuery, + queries::idty::IdtyQuery, + queries::network::NetworkQuery, + queries::txs_history::TxsHistoryBlockchainQuery, + queries::txs_history::TxsHistoryMempoolQuery, + queries::uds::UdsQuery, + queries::utxos_of_script::UtxosQuery, +); + +#[derive(Default, async_graphql::SimpleObject)] +struct NodeQuery { + node: Node, +} + +#[derive(Default)] +struct Node; + +#[async_graphql::Object] +impl Node { + /// Peer card + async fn peer( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Option<PeerCardGva>> { + let data = ctx.data::<GvaSchemaData>()?; + + if let Some(self_peer_old) = data + .cm_accessor() + .get_self_peer_old(|self_peer_old| self_peer_old.clone()) + .await + { + Ok(Some(PeerCardGva::from(self_peer_old))) + } else { + Ok(None) + } + } + /// Software + async fn software(&self) -> &'static str { + duniter_core::module::SOFTWARE_NAME + } + /// Software version + async fn version( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<&'static str> { + let data = ctx.data::<GvaSchemaData>()?; + Ok(data.server_meta_data.software_version) + } +} diff --git a/gql/src/queries/account_balance.rs b/gql/src/queries/account_balance.rs new file mode 100644 index 0000000000000000000000000000000000000000..81d72677f0a85a5be868dc512787d8e992f6147c --- /dev/null +++ b/gql/src/queries/account_balance.rs @@ -0,0 +1,143 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default)] +pub(crate) struct AccountBalanceQuery; +#[async_graphql::Object] +impl AccountBalanceQuery { + /// Get the balance of an account identified by a public key or a script. + /// + /// If the balance is null, the account has never been used. If the balance is zero, the account has already transited money in the past. + async fn balance( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Account script or public key")] script: PkOrScriptGva, + ) -> async_graphql::Result<Option<AmountWithBase>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + Ok(data + .dbs_pool + .execute(move |_| dbs_reader.get_account_balance(&script.0)) + .await?? + .map(|balance| AmountWithBase { + amount: balance.0.amount() as i32, + base: balance.0.base() as i32, + })) + } + /// Get the balance of several accounts in a single request + /// + /// Each account can be identified by a public key or a script. It is possible to mix the two in the same request. + /// The balances are returned in the order of the accounts provided as input. Each account has a balance, + /// which is null if the account does not exist. + async fn balances( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Accounts scripts or publics keys")] scripts: Vec<PkOrScriptGva>, + ) -> async_graphql::Result<Vec<Option<AmountWithBase>>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + Ok(data + .dbs_pool + .execute(move |_| { + scripts + .iter() + .map(|account_script| { + dbs_reader + .get_account_balance(&account_script.0) + .map(|balance_opt| { + balance_opt.map(|balance| AmountWithBase { + amount: balance.0.amount() as i32, + base: balance.0.base() as i32, + }) + }) + }) + .collect::<Result<Vec<_>, _>>() + }) + .await??) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + use duniter_core::dbs::SourceAmountValV2; + + #[tokio::test] + async fn query_balance() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_get_account_balance() + .withf(|s| { + s == &WalletScriptV10::single_sig( + PublicKey::from_base58("DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP") + .expect("wrong pubkey"), + ) + }) + .times(1) + .returning(|_| Ok(Some(SourceAmountValV2(SourceAmount::with_base0(38))))); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ balance(script: "DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP") {amount} }"# + ) + .await?, + serde_json::json!({ + "data": { + "balance": { + "amount": 38 + } + } + }) + ); + Ok(()) + } + + #[tokio::test] + async fn query_balances() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_get_account_balance() + .withf(|s| { + s == &WalletScriptV10::single_sig( + PublicKey::from_base58("DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP") + .expect("wrong pubkey"), + ) + }) + .times(1) + .returning(|_| Ok(Some(SourceAmountValV2(SourceAmount::with_base0(38))))); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ balances(scripts: ["DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP"]) {amount} }"# + ) + .await?, + serde_json::json!({ + "data": { + "balances": [{ + "amount": 38 + }] + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/block.rs b/gql/src/queries/block.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5cf8ea492e1bd960f076c2863bbed8f0618dd49 --- /dev/null +++ b/gql/src/queries/block.rs @@ -0,0 +1,142 @@ +// Copyright (C) 2021 Pascal Engélibert +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use duniter_gva_dbs_reader::PagedData; + +#[derive(Default)] +pub(crate) struct BlockQuery; +#[async_graphql::Object] +impl BlockQuery { + /// Get block by number + async fn block_by_number( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "block number")] number: u32, + ) -> async_graphql::Result<Option<BlockMeta>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + let block = data + .dbs_pool + .execute(move |dbs| dbs_reader.block(&dbs.bc_db_ro, U32BE(number))) + .await??; + + Ok(block.map(|block| BlockMeta::from(&block))) + } + + /// Get blocks + async fn blocks( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "pagination", default)] pagination: Pagination, + ) -> async_graphql::Result<Connection<String, BlockMeta, EmptyFields, EmptyFields>> { + let QueryContext { is_whitelisted } = ctx.data::<QueryContext>()?; + let page_info = Pagination::convert_to_page_info(pagination, *is_whitelisted)?; + + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + let PagedData { + data: blocks, + has_next_page, + has_previous_page, + } = data + .dbs_pool + .execute(move |dbs| dbs_reader.blocks(&dbs.bc_db_ro, page_info)) + .await??; + + let mut conn = Connection::new(has_previous_page, has_next_page); + + conn.append(blocks.into_iter().map(|(block_cursor, block)| { + Edge::new(block_cursor.to_string(), BlockMeta::from(block)) + })); + + Ok(conn) + } +} + +#[cfg(test)] +mod tests { + use super::BlockNumber; + use crate::tests::*; + use duniter_core::dbs::BlockMetaV2; + use duniter_gva_dbs_reader::{block::BlockCursor, PagedData}; + + #[tokio::test] + async fn test_block_by_number() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_block() + .withf(|_, s| s.0 == 0) + .times(1) + .returning(|_, _| Ok(Some(BlockMetaV2::default()))); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request(&schema, r#"{ blockByNumber(number: 0) {number} }"#).await?, + serde_json::json!({ + "data": { + "blockByNumber": { + "number": BlockMetaV2::default().number, + } + } + }) + ); + Ok(()) + } + + #[tokio::test] + async fn test_blocks() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader.expect_blocks().times(1).returning(|_, _| { + Ok(PagedData { + data: vec![( + BlockCursor { + number: BlockNumber(0), + }, + BlockMetaV2::default(), + )], + has_next_page: false, + has_previous_page: false, + }) + }); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ blocks{pageInfo{startCursor,endCursor},edges{node{number}}} }"# + ) + .await?, + serde_json::json!({ + "data": { + "blocks": { + "edges": [ + { + "node": { + "number": BlockMetaV2::default().number, + } + } + ], + "pageInfo": { + "endCursor": BlockMetaV2::default().number.to_string(), + "startCursor": BlockMetaV2::default().number.to_string(), + } + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/current_block.rs b/gql/src/queries/current_block.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d5ed16dc73bba99012bec34059229e73545c320 --- /dev/null +++ b/gql/src/queries/current_block.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default)] +pub(crate) struct CurrentBlockQuery; +#[async_graphql::Object] +impl CurrentBlockQuery { + /// Get current block + async fn current_block( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<BlockMeta> { + let data = ctx.data::<GvaSchemaData>()?; + + if let Some(current_block_meta) = data + .cm_accessor() + .get_current_meta(|cm| cm.current_block_meta) + .await + { + Ok(current_block_meta.into()) + } else { + Err(async_graphql::Error::new("no blockchain")) + } + } +} + +#[cfg(test)] +mod tests { + use crate::tests::*; + + #[tokio::test] + async fn query_current_block() -> anyhow::Result<()> { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<duniter_core::dbs::BlockMetaV2>() + .times(1) + .returning(|f| Some(f(&CurrentMeta::default()))); + let schema = create_schema(mock_cm, MockDbsReader::new())?; + assert_eq!( + exec_graphql_request(&schema, r#"{ currentBlock {nonce} }"#).await?, + serde_json::json!({ + "data": { + "currentBlock": { + "nonce": 0 + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/current_frame.rs b/gql/src/queries/current_frame.rs new file mode 100644 index 0000000000000000000000000000000000000000..5abca6b36790b815b8236f19add00d5a2564ee3a --- /dev/null +++ b/gql/src/queries/current_frame.rs @@ -0,0 +1,94 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default)] +pub(crate) struct CurrentFrameQuery; +#[async_graphql::Object] +impl CurrentFrameQuery { + /// Get blocks in current frame + async fn current_frame( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Vec<BlockMeta>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + if let Some(current_block_meta) = data + .cm_accessor() + .get_current_meta(|cm| cm.current_block_meta) + .await + { + Ok(data + .dbs_pool + .execute(move |dbs| { + dbs_reader.get_current_frame(&dbs.bc_db_ro, ¤t_block_meta) + }) + .await?? + .into_iter() + .map(Into::into) + .collect()) + } else { + Ok(vec![]) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + use duniter_core::dbs::databases::bc_v2::BcV2DbRo; + use duniter_core::dbs::BlockMetaV2; + + #[tokio::test] + async fn query_current_frame() -> anyhow::Result<()> { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<BlockMetaV2>() + .times(1) + .returning(|f| { + Some(f(&CurrentMeta { + current_block_meta: BlockMetaV2 { + issuers_frame: 1, + ..Default::default() + }, + ..Default::default() + })) + }); + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_get_current_frame::<BcV2DbRo<FileBackend>>() + .times(1) + .returning(|_, _| { + Ok(vec![BlockMetaV2 { + ..Default::default() + }]) + }); + let schema = create_schema(mock_cm, dbs_reader)?; + assert_eq!( + exec_graphql_request(&schema, r#"{ currentFrame {nonce} }"#).await?, + serde_json::json!({ + "data": { + "currentFrame": [{ + "nonce": 0 + }] + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/first_utxos_of_scripts.rs b/gql/src/queries/first_utxos_of_scripts.rs new file mode 100644 index 0000000000000000000000000000000000000000..a8d3c1bd20f33cd0f593548b2cce539bdd9595f9 --- /dev/null +++ b/gql/src/queries/first_utxos_of_scripts.rs @@ -0,0 +1,65 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default)] +pub(crate) struct FirstUtxosQuery; +#[async_graphql::Object] +impl FirstUtxosQuery { + /// First utxos of scripts + async fn first_utxos_of_scripts( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql( + desc = "DUBP wallets scripts", + validator(ListMaxLength(length = "100")) + )] + scripts: Vec<PkOrScriptGva>, + #[graphql( + desc = "Number of first utxos to get ", + default = 10, + validator(IntRange(min = "1", max = "40")) + )] + first: i32, + ) -> async_graphql::Result<Vec<UtxosGva>> { + let scripts: Vec<WalletScriptV10> = scripts.into_iter().map(|script| script.0).collect(); + + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + + let utxos_matrice: Vec<arrayvec::ArrayVec<_>> = data + .dbs_pool + .execute(move |_| db_reader.first_scripts_utxos(None, first as usize, &scripts)) + .await??; + + Ok(utxos_matrice + .into_iter() + .map(|utxos| { + UtxosGva( + utxos + .into_iter() + .map(|utxo| UtxoGva { + amount: utxo.amount.amount(), + base: utxo.amount.base(), + tx_hash: utxo.tx_hash.to_hex(), + output_index: utxo.output_index as u32, + }) + .collect(), + ) + }) + .collect()) + } +} diff --git a/gql/src/queries/gen_tx.rs b/gql/src/queries/gen_tx.rs new file mode 100644 index 0000000000000000000000000000000000000000..7f4a1b981ad287327517c7274b11117e9dece5ef --- /dev/null +++ b/gql/src/queries/gen_tx.rs @@ -0,0 +1,277 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use dubp::{ + crypto::bases::BaseConversionError, + documents::transaction::v10_gen::{TransactionDocV10ComplexGen, TxV10ComplexIssuer}, + documents::transaction::UnsignedTransactionDocumentTrait, +}; +use duniter_core::dbs::smallvec::SmallVec; + +struct TxIssuerTyped { + script: WalletScriptV10, + signers: SmallVec<[PublicKey; 1]>, + codes: SmallVec<[String; 1]>, + amount: i32, +} +impl TryFrom<TxIssuer> for TxIssuerTyped { + type Error = async_graphql::Error; + + fn try_from(input: TxIssuer) -> async_graphql::Result<Self> { + let codes = if let Some(codes) = input.codes { + codes.into_iter().collect() + } else { + SmallVec::new() + }; + let signers: SmallVec<[PublicKey; 1]> = input + .signers + .iter() + .map(|s| PublicKey::from_base58(s)) + .collect::<Result<_, BaseConversionError>>()?; + let script = if let Some(ref script_str) = input.script { + dubp::documents_parser::wallet_script_from_str(script_str)? + } else if signers.len() <= 3 && codes.is_empty() { + match signers.len() { + 1 => WalletScriptV10::single(WalletConditionV10::Sig(signers[0])), + 2 => WalletScriptV10::and( + WalletConditionV10::Sig(signers[0]), + WalletConditionV10::Sig(signers[1]), + ), + 3 => WalletScriptV10::and_and( + WalletConditionV10::Sig(signers[0]), + WalletConditionV10::Sig(signers[1]), + WalletConditionV10::Sig(signers[2]), + ), + _ => unreachable!(), + } + } else { + return Err(async_graphql::Error::new("missing a issuer script")); + }; + Ok(Self { + script, + signers, + codes, + amount: input.amount, + }) + } +} +struct TxRecipientTyped { + amount: i32, + script: WalletScriptV10, +} +impl TryFrom<TxRecipient> for TxRecipientTyped { + type Error = async_graphql::Error; + + fn try_from(input: TxRecipient) -> async_graphql::Result<Self> { + let script = dubp::documents_parser::wallet_script_from_str(&input.script)?; + Ok(Self { + amount: input.amount, + script, + }) + } +} + +#[derive(Default)] +pub(crate) struct GenTxsQuery; +#[async_graphql::Object] +impl GenTxsQuery { + #[allow(clippy::too_many_arguments)] + /// Generate simple transaction document + async fn gen_tx( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Transaction amount", validator(IntGreaterThan(value = "0")))] amount: i32, + #[graphql( + desc = "Cash back address, equal to issuer address by default (Ed25519 public key on base 58 representation)" + )] + cash_back_address: Option<PubKeyGva>, + #[graphql(desc = "Transaction comment", validator(TxCommentValidator))] comment: Option< + String, + >, + issuer: PubKeyGva, + #[graphql(desc = "Recipient address")] recipient: PkOrScriptGva, + #[graphql(desc = "Use mempool sources", default = false)] use_mempool_sources: bool, + ) -> async_graphql::Result<Vec<String>> { + let comment = comment.unwrap_or_default(); + let issuer = issuer.0; + let recipient = recipient.0; + + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + let currency = data.server_meta_data.currency.clone(); + + if let Some(current_block_meta) = data + .cm_accessor + .get_current_meta(|cm| cm.current_block_meta) + .await + { + let (inputs, inputs_sum) = data + .dbs_pool + .execute(move |dbs| { + db_reader.find_inputs( + &dbs.bc_db_ro, + &dbs.txs_mp_db, + SourceAmount::new(amount as i64, current_block_meta.unit_base as i64), + &WalletScriptV10::single(WalletConditionV10::Sig(issuer)), + use_mempool_sources, + ) + }) + .await??; + + let amount = SourceAmount::new(amount as i64, current_block_meta.unit_base as i64); + + if inputs_sum < amount { + return Err(async_graphql::Error::new("insufficient balance")); + } + + let current_blockstamp = Blockstamp { + number: BlockNumber(current_block_meta.number), + hash: BlockHash(current_block_meta.hash), + }; + + Ok(TransactionDocumentV10::generate_simple_txs( + current_blockstamp, + currency, + (inputs, inputs_sum), + issuer, + recipient, + (amount, comment), + cash_back_address.map(|pubkey_gva| pubkey_gva.0), + ) + .into_iter() + .map(|tx| tx.as_text().to_owned()) + .collect()) + } else { + Err(async_graphql::Error::new("no blockchain")) + } + } + /// Generate complex transaction document + async fn gen_complex_tx( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Transaction issuers")] issuers: Vec<TxIssuer>, + #[graphql(desc = "Transaction recipients")] recipients: Vec<TxRecipient>, + #[graphql(desc = "Transaction comment", validator(TxCommentValidator))] comment: Option< + String, + >, + #[graphql(desc = "Use mempool sources", default = false)] use_mempool_sources: bool, + ) -> async_graphql::Result<RawTxOrChanges> { + let comment = comment.unwrap_or_default(); + let issuers = issuers + .into_iter() + .map(TryFrom::try_from) + .collect::<async_graphql::Result<Vec<TxIssuerTyped>>>()?; + let recipients = recipients + .into_iter() + .map(TryFrom::try_from) + .collect::<async_graphql::Result<Vec<TxRecipientTyped>>>()?; + + let issuers_sum: i32 = issuers.iter().map(|issuer| issuer.amount).sum(); + let recipients_sum: i32 = recipients.iter().map(|recipient| recipient.amount).sum(); + if issuers_sum != recipients_sum { + return Err(async_graphql::Error::new( + "The sum of the amounts of the issuers must be equal to the sum of the amounts of the recipients.", + )); + } + + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + let currency = data.server_meta_data.currency.clone(); + + if let Some(current_block_meta) = data + .cm_accessor + .get_current_meta(|cm| cm.current_block_meta) + .await + { + let issuers_inputs_with_sum = data + .dbs_pool + .execute(move |dbs| { + let mut issuers_inputs_with_sum = Vec::new(); + for issuer in issuers { + issuers_inputs_with_sum.push(( + db_reader.find_inputs( + &dbs.bc_db_ro, + &dbs.txs_mp_db, + SourceAmount::new( + issuer.amount as i64, + current_block_meta.unit_base as i64, + ), + &issuer.script, + use_mempool_sources, + )?, + issuer, + )); + } + Ok::<_, anyhow::Error>(issuers_inputs_with_sum) + }) + .await??; + + for ((_inputs, inputs_sum), issuer) in &issuers_inputs_with_sum { + let amount = + SourceAmount::new(issuer.amount as i64, current_block_meta.unit_base as i64); + if *inputs_sum < amount { + return Err(async_graphql::Error::new(format!( + "Insufficient balance for issuer {}", + issuer.script.to_string() + ))); + } + } + + let current_blockstamp = Blockstamp { + number: BlockNumber(current_block_meta.number), + hash: BlockHash(current_block_meta.hash), + }; + let base = current_block_meta.unit_base as i64; + + let (final_tx_opt, changes_txs) = TransactionDocV10ComplexGen { + blockstamp: current_blockstamp, + currency, + issuers: issuers_inputs_with_sum + .into_iter() + .map(|((inputs, inputs_sum), issuer)| TxV10ComplexIssuer { + amount: SourceAmount::new(issuer.amount as i64, base), + codes: issuer.codes, + inputs, + inputs_sum, + script: issuer.script, + signers: issuer.signers, + }) + .collect(), + recipients: recipients + .into_iter() + .map(|TxRecipientTyped { amount, script }| { + (SourceAmount::new(amount as i64, base), script) + }) + .collect(), + user_comment: comment, + } + .gen()?; + + if let Some(final_tx) = final_tx_opt { + Ok(RawTxOrChanges::FinalTx(final_tx.as_text().to_owned())) + } else { + Ok(RawTxOrChanges::Changes( + changes_txs + .into_iter() + .map(|tx| tx.as_text().to_owned()) + .collect(), + )) + } + } else { + Err(async_graphql::Error::new("no blockchain")) + } + } +} diff --git a/gql/src/queries/idty.rs b/gql/src/queries/idty.rs new file mode 100644 index 0000000000000000000000000000000000000000..b2117f6895378917a30b2f1bfddbac55ed4ac5bc --- /dev/null +++ b/gql/src/queries/idty.rs @@ -0,0 +1,81 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default)] +pub(crate) struct IdtyQuery; +#[async_graphql::Object] +impl IdtyQuery { + /// Get identity by public key + async fn idty( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "public key")] pubkey: PubKeyGva, + ) -> async_graphql::Result<Option<Identity>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + Ok(data + .dbs_pool + .execute(move |dbs| dbs_reader.idty(&dbs.bc_db_ro, pubkey.0)) + .await?? + .map(|idty| Identity { + is_member: idty.is_member, + username: idty.username, + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[tokio::test] + async fn test_idty() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_idty() + .withf(|_, s| { + s == &PublicKey::from_base58("DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP") + .expect("wrong pubkey") + }) + .times(1) + .returning(|_, _| { + Ok(Some(duniter_core::dbs::IdtyDbV2 { + is_member: true, + username: String::from("JohnDoe"), + })) + }); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ idty(pubkey: "DnjL6hYA1k7FavGHbbir79PKQbmzw63d6bsamBBdUULP") {isMember, username} }"# + ) + .await?, + serde_json::json!({ + "data": { + "idty": { + "isMember": true, + "username": "JohnDoe" + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/network.rs b/gql/src/queries/network.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b4d1a0b04bc074fa26f2a3c85dceff52b0719f2 --- /dev/null +++ b/gql/src/queries/network.rs @@ -0,0 +1,144 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +#[derive(Default, async_graphql::SimpleObject)] +pub(crate) struct NetworkQuery { + network: NetworkQueryInner, +} + +#[derive(Default)] +pub(crate) struct NetworkQueryInner; + +#[async_graphql::Object] +impl NetworkQueryInner { + /// Get endpoints known by the node + async fn endpoints( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql( + desc = "filter endpoints by api (exact match endpoint first word, case sensitive)" + )] + api_list: Vec<String>, + ) -> async_graphql::Result<Vec<String>> { + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + Ok(data + .dbs_pool + .execute(move |dbs| dbs_reader.endpoints(&dbs.dunp_db, api_list)) + .await??) + } + /// Get peers and heads + async fn nodes( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Vec<PeerWithHeads>> { + let data = ctx.data::<GvaSchemaData>()?; + + let db_reader = data.dbs_reader(); + + Ok(data + .dbs_pool + .execute(move |dbs| db_reader.peers_and_heads(&dbs.dunp_db)) + .await?? + .into_iter() + .map(|(peer, heads)| PeerWithHeads { + peer: PeerCardGva::from(peer), + heads: heads.into_iter().map(HeadGva::from).collect(), + }) + .collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + use duniter_core::dbs::databases::network_v1::NetworkV1Db; + use pretty_assertions::assert_eq; + + #[tokio::test] + async fn test_endpoints() -> anyhow::Result<()> { + let mock_cm = MockAsyncAccessor::new(); + let mut mock_dbs_reader = MockDbsReader::new(); + mock_dbs_reader + .expect_endpoints::<NetworkV1Db<FileBackend>>() + .times(1) + .returning(|_, _| { + Ok(vec![ + "GVA S g1.librelois.fr 443 gva".to_owned(), + "GVA S domain.tld 443 gva".to_owned(), + ]) + }); + let schema = create_schema(mock_cm, mock_dbs_reader)?; + assert_eq!( + exec_graphql_request(&schema, r#"{ network { endpoints(apiList:["GVA"]) } }"#).await?, + serde_json::json!({ + "data": { + "network": { + "endpoints": [ + "GVA S g1.librelois.fr 443 gva", + "GVA S domain.tld 443 gva" + ] + } + } + }) + ); + Ok(()) + } + + #[tokio::test] + async fn test_peers_and_heads() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_peers_and_heads::<NetworkV1Db<FileBackend>>() + .times(1) + .returning(|_| { + Ok(vec![( + duniter_core::dbs::PeerCardDbV1::default(), + vec![duniter_core::dbs::DunpHeadDbV1::default()], + )]) + }); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ network { nodes { peer { blockstamp }, heads { blockstamp } } } }"# + ) + .await?, + serde_json::json!({ + "data": { + "network": { + "nodes": [ + { + "heads": [ + { + "blockstamp": "0-0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "peer": { + "blockstamp": "" + } + } + ], + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/txs_history.rs b/gql/src/queries/txs_history.rs new file mode 100644 index 0000000000000000000000000000000000000000..3cf5bfe93b2b0941d84dc27897555dceb4f7b3ed --- /dev/null +++ b/gql/src/queries/txs_history.rs @@ -0,0 +1,345 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::inputs::TimeInterval; +use crate::*; +use duniter_gva_db::GvaTxDbV1; +use duniter_gva_dbs_reader::txs_history::TxBcCursor; +use futures::future::join; + +#[derive(Default)] +pub(crate) struct TxsHistoryBlockchainQuery; + +#[async_graphql::Object] +impl TxsHistoryBlockchainQuery { + /// Transactions history (written in blockchain) + async fn txs_history_bc( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "pagination", default)] pagination: Pagination, + script: PkOrScriptGva, + #[graphql(default)] time_interval: TimeInterval, + ) -> async_graphql::Result<TxsHistoryBlockchainQueryInner> { + let QueryContext { is_whitelisted } = ctx.data::<QueryContext>()?; + let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?; + let script_hash = Hash::compute(script.0.to_string().as_bytes()); + Ok(TxsHistoryBlockchainQueryInner { + pagination, + script_hash, + time_interval, + }) + } +} + +pub(crate) struct TxsHistoryBlockchainQueryInner { + pub(crate) pagination: PageInfo<TxBcCursor>, + pub(crate) script_hash: Hash, + pub(crate) time_interval: TimeInterval, +} + +#[async_graphql::Object] +impl TxsHistoryBlockchainQueryInner { + /// Transactions history (written in blockchain) + async fn both( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Connection<String, TxGva, EmptyFields, EdgeTx>> { + let start_time = std::time::Instant::now(); + + let data = ctx.data::<GvaSchemaData>()?; + + let db_reader = data.dbs_reader(); + let pagination = self.pagination; + let script_hash = self.script_hash; + let time_interval = self.time_interval; + let sent_fut = data.dbs_pool.execute(move |_| { + db_reader.get_txs_history_bc_sent( + time_interval.from, + pagination, + script_hash, + time_interval.to, + ) + }); + let db_reader = data.dbs_reader(); + let script_hash = self.script_hash; + let received_fut = data.dbs_pool.execute(move |_| { + db_reader.get_txs_history_bc_received( + time_interval.from, + pagination, + script_hash, + time_interval.to, + ) + }); + let (sent_res, received_res) = join(sent_fut, received_fut).await; + let (sent, received) = (sent_res??, received_res??); + + let mut both_txs = sent + .data + .into_iter() + .map(|db_tx| (TxDirection::Sent, db_tx)) + .chain( + received + .data + .into_iter() + .map(|db_tx| (TxDirection::Received, db_tx)), + ) + .collect::<Vec<(TxDirection, GvaTxDbV1)>>(); + /*if let Some(TxBcCursor { tx_hash, .. }) = pagination.pos() { + while both.txs + }*/ + if self.pagination.order() { + both_txs.sort_unstable_by(|(_, db_tx1), (_, db_tx2)| { + db_tx1 + .written_block + .number + .cmp(&db_tx2.written_block.number) + }); + } else { + both_txs.sort_unstable_by(|(_, db_tx1), (_, db_tx2)| { + db_tx2 + .written_block + .number + .cmp(&db_tx1.written_block.number) + }); + } + if let Some(limit) = self.pagination.limit_opt() { + both_txs.truncate(limit.get()); + } + let mut conn = Connection::new( + sent.has_previous_page || received.has_previous_page, + sent.has_next_page || received.has_next_page, + ); + conn.append(both_txs.into_iter().map(|(tx_direction, db_tx)| { + Edge::with_additional_fields( + TxBcCursor { + block_number: db_tx.written_block.number, + tx_hash: db_tx.tx.get_hash(), + } + .to_string(), + db_tx.into(), + EdgeTx { + direction: tx_direction, + }, + ) + })); + + println!( + "txs_history_bc::both duration: {}ms", + start_time.elapsed().as_millis() + ); + + Ok(conn) + } + /// Received transactions history (written in blockchain) + async fn received( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Connection<String, TxGva, EmptyFields, EmptyFields>> { + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + let pagination = self.pagination; + let script_hash = self.script_hash; + let time_interval = self.time_interval; + let received = data + .dbs_pool + .execute(move |_| { + db_reader.get_txs_history_bc_received( + time_interval.from, + pagination, + script_hash, + time_interval.to, + ) + }) + .await??; + let mut conn = Connection::new(received.has_previous_page, received.has_next_page); + conn.append(received.data.into_iter().map(|db_tx| { + Edge::new( + TxBcCursor { + block_number: db_tx.written_block.number, + tx_hash: db_tx.tx.get_hash(), + } + .to_string(), + db_tx.into(), + ) + })); + + Ok(conn) + } + /// Sent transactions history (written in blockchain) + async fn sent( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Connection<String, TxGva, EmptyFields, EmptyFields>> { + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + let pagination = self.pagination; + let script_hash = self.script_hash; + let time_interval = self.time_interval; + let sent = data + .dbs_pool + .execute(move |_| { + db_reader.get_txs_history_bc_sent( + time_interval.from, + pagination, + script_hash, + time_interval.to, + ) + }) + .await??; + let mut conn = Connection::new(sent.has_previous_page, sent.has_next_page); + conn.append(sent.data.into_iter().map(|db_tx| { + Edge::new( + TxBcCursor { + block_number: db_tx.written_block.number, + tx_hash: db_tx.tx.get_hash(), + } + .to_string(), + db_tx.into(), + ) + })); + + Ok(conn) + } +} + +#[derive(Default)] +pub(crate) struct TxsHistoryMempoolQuery; + +#[async_graphql::Object] +impl TxsHistoryMempoolQuery { + /// Transactions waiting on mempool + async fn txs_history_mp( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Ed25519 public key on base 58 representation")] pubkey: PubKeyGva, + ) -> async_graphql::Result<TxsHistoryMempool> { + let data = ctx.data::<GvaSchemaData>()?; + let db_reader = data.dbs_reader(); + + let (sending, pending) = data + .dbs_pool + .execute(move |dbs| db_reader.get_txs_history_mempool(&dbs.txs_mp_db, pubkey.0)) + .await??; + + Ok(TxsHistoryMempool { + sending: sending + .into_iter() + .map(|db_tx| TxGva::from(&db_tx)) + .collect(), + receiving: pending + .into_iter() + .map(|db_tx| TxGva::from(&db_tx)) + .collect(), + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::VecDeque; + + use crate::tests::*; + use dubp::documents::transaction::TransactionDocumentV10; + use dubp::documents::transaction::TransactionDocumentV10Stringified; + use dubp::documents_parser::prelude::FromStringObject; + use duniter_gva_db::GvaTxDbV1; + use duniter_gva_dbs_reader::pagination::PagedData; + + #[tokio::test] + async fn test_txs_history_blockchain() -> anyhow::Result<()> { + let mut dbs_reader = MockDbsReader::new(); + dbs_reader + .expect_get_txs_history_bc_received() + .times(1) + .returning(|_, _, _, _| Ok(PagedData::empty())); + dbs_reader + .expect_get_txs_history_bc_sent() + .times(1) + .returning(|_, _, _, _| { + let tx = TransactionDocumentV10::from_string_object( + &TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: + "0-0000000000000000000000000000000000000000000000000000000000000000" + .to_owned(), + locktime: 0, + issuers: vec![], + inputs: vec![], + unlocks: vec![], + outputs: vec![], + comment: "".to_owned(), + signatures: vec![], + hash: Some( + "0000000000000000000000000000000000000000000000000000000000000000" + .to_owned(), + ), + }, + ) + .expect("wrong tx"); + let mut expected_data = VecDeque::new(); + expected_data.push_back(GvaTxDbV1 { + tx, + ..Default::default() + }); + Ok(PagedData { + data: expected_data, + has_previous_page: false, + has_next_page: false, + }) + }); + let schema = create_schema(MockAsyncAccessor::new(), dbs_reader)?; + assert_eq!( + exec_graphql_request( + &schema, + r#"{ + txsHistoryBc(script: "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx") { + sent { + edges { + node { + blockstamp + } + } + } + received { + edges { + node { + blockstamp + } + } + } + } + }"# + ) + .await?, + serde_json::json!({ + "data": { + "txsHistoryBc": { + "received": { + "edges": [] + }, + "sent": { + "edges": [{ + "node": { + "blockstamp": "0-0000000000000000000000000000000000000000000000000000000000000000", + } + }] + } + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/uds.rs b/gql/src/queries/uds.rs new file mode 100644 index 0000000000000000000000000000000000000000..b24f90c8682cc777108d22f37cd55e4c1b91e2af --- /dev/null +++ b/gql/src/queries/uds.rs @@ -0,0 +1,185 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use async_graphql::connection::*; +use duniter_core::dbs::databases::bc_v2::BcV2DbReadable; +use duniter_gva_dbs_reader::{uds_of_pubkey::UdsWithSum, PagedData}; + +#[derive(Default)] +pub(crate) struct UdsQuery; +#[async_graphql::Object] +impl UdsQuery { + /// Current universal dividends amount + async fn current_ud( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Option<CurrentUdGva>> { + let data = ctx.data::<GvaSchemaData>()?; + + Ok( + if let Some(current_ud) = data.cm_accessor.get_current_meta(|cm| cm.current_ud).await { + Some(CurrentUdGva { + amount: current_ud.amount(), + base: current_ud.base(), + }) + } else { + None + }, + ) + } + /// Universal dividends issued by a public key + #[allow(clippy::clippy::too_many_arguments)] + async fn uds_of_pubkey( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "Ed25519 public key on base 58 representation")] pubkey: PubKeyGva, + #[graphql(default)] filter: UdsFilter, + #[graphql(desc = "pagination", default)] pagination: Pagination, + #[graphql(desc = "Amount needed")] amount: Option<i64>, + ) -> async_graphql::Result<Connection<String, UdGva, AggregateSum, EmptyFields>> { + let QueryContext { is_whitelisted } = ctx.data::<QueryContext>()?; + let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?; + + let data = ctx.data::<GvaSchemaData>()?; + let dbs_reader = data.dbs_reader(); + + if let Some(current_base) = data + .cm_accessor + .get_current_meta(|cm| cm.current_block_meta.unit_base) + .await + { + let ( + PagedData { + data: UdsWithSum { uds, sum }, + has_previous_page, + has_next_page, + }, + times, + ) = data + .dbs_pool + .execute(move |dbs| { + let paged_data = match filter { + UdsFilter::All => { + dbs_reader.all_uds_of_pubkey(&dbs.bc_db_ro, pubkey.0, pagination) + } + UdsFilter::Unspent => dbs_reader.unspent_uds_of_pubkey( + &dbs.bc_db_ro, + pubkey.0, + pagination, + None, + amount.map(|amount| SourceAmount::new(amount, current_base as i64)), + ), + }?; + + let mut times = Vec::with_capacity(paged_data.data.uds.len()); + for (bn, _sa) in &paged_data.data.uds { + times.push(dbs_reader.get_blockchain_time(*bn)?); + } + Ok::<_, anyhow::Error>((paged_data, times)) + }) + .await??; + + let mut conn = Connection::with_additional_fields( + has_previous_page, + has_next_page, + AggregateSum { + aggregate: Sum { + sum: AmountWithBase { + amount: sum.amount() as i32, + base: sum.base() as i32, + }, + }, + }, + ); + let uds_timed = + uds.into_iter() + .zip(times.into_iter()) + .map(|((bn, sa), blockchain_time)| { + Edge::new( + bn.0.to_string(), + UdGva { + amount: sa.amount(), + base: sa.base(), + issuer: pubkey, + block_number: bn.0, + blockchain_time, + }, + ) + }); + if pagination.order() { + conn.append(uds_timed); + } else { + conn.append(uds_timed.rev()); + } + Ok(conn) + } else { + Err(async_graphql::Error::new("no blockchain")) + } + } + /// Universal dividends revaluations + async fn uds_reval( + &self, + ctx: &async_graphql::Context<'_>, + ) -> async_graphql::Result<Vec<RevalUdGva>> { + let data = ctx.data::<GvaSchemaData>()?; + + Ok(data + .dbs_pool + .execute(move |dbs| { + dbs.bc_db_ro.uds_reval().iter(.., |it| { + it.map_ok(|(block_number, sa)| RevalUdGva { + amount: sa.0.amount(), + base: sa.0.base(), + block_number: block_number.0, + }) + .collect::<KvResult<Vec<_>>>() + }) + }) + .await??) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[tokio::test] + async fn query_current_ud() -> anyhow::Result<()> { + let mut mock_cm = MockAsyncAccessor::new(); + mock_cm + .expect_get_current_meta::<SourceAmount>() + .times(1) + .returning(|f| { + Some(f(&CurrentMeta { + current_ud: SourceAmount::with_base0(100), + ..Default::default() + })) + }); + let schema = create_schema(mock_cm, MockDbsReader::new())?; + assert_eq!( + exec_graphql_request(&schema, r#"{ currentUd {amount} }"#).await?, + serde_json::json!({ + "data": { + "currentUd": { + "amount": 100 + } + } + }) + ); + Ok(()) + } +} diff --git a/gql/src/queries/utxos_of_script.rs b/gql/src/queries/utxos_of_script.rs new file mode 100644 index 0000000000000000000000000000000000000000..c3a3039e5f374ab070eb51642aba24f2bff37f1e --- /dev/null +++ b/gql/src/queries/utxos_of_script.rs @@ -0,0 +1,103 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use async_graphql::connection::*; +use duniter_gva_dbs_reader::{ + utxos::{UtxoCursor, UtxosWithSum}, + PagedData, +}; + +#[derive(Default)] +pub(crate) struct UtxosQuery; +#[async_graphql::Object] +impl UtxosQuery { + /// Utxos of script + async fn utxos_of_script( + &self, + ctx: &async_graphql::Context<'_>, + #[graphql(desc = "DUBP wallet script")] script: PkOrScriptGva, + #[graphql(desc = "pagination", default)] pagination: Pagination, + #[graphql(desc = "Amount needed")] amount: Option<i64>, + ) -> async_graphql::Result<Connection<String, UtxoTimedGva, AggregateSum, EmptyFields>> { + let QueryContext { is_whitelisted } = ctx.data::<QueryContext>()?; + log::info!("is_whitelisted={}", is_whitelisted); + let pagination = Pagination::convert_to_page_info(pagination, *is_whitelisted)?; + + let data = ctx.data::<GvaSchemaData>()?; + let cm_accessor = data.cm_accessor(); + let db_reader = data.dbs_reader(); + + if let Some(current_base) = cm_accessor + .get_current_meta(|cm| cm.current_block_meta.unit_base) + .await + { + let ( + PagedData { + data: UtxosWithSum { utxos, sum }, + has_previous_page, + has_next_page, + }, + times, + ) = data + .dbs_pool + .execute(move |dbs| { + let paged_data = db_reader.find_script_utxos( + &dbs.txs_mp_db, + amount.map(|amount| SourceAmount::new(amount, current_base as i64)), + pagination, + &script.0, + )?; + let mut times = Vec::with_capacity(paged_data.data.utxos.len()); + for (UtxoCursor { block_number, .. }, _sa) in &paged_data.data.utxos { + times.push(db_reader.get_blockchain_time(*block_number)?); + } + Ok::<_, anyhow::Error>((paged_data, times)) + }) + .await??; + + let mut conn = Connection::with_additional_fields( + has_previous_page, + has_next_page, + AggregateSum { + aggregate: Sum { + sum: AmountWithBase { + amount: sum.amount() as i32, + base: sum.base() as i32, + }, + }, + }, + ); + conn.append(utxos.into_iter().zip(times.into_iter()).map( + |((utxo_cursor, source_amount), blockchain_time)| { + Edge::new( + utxo_cursor.to_string(), + UtxoTimedGva { + amount: source_amount.amount(), + base: source_amount.base(), + tx_hash: utxo_cursor.tx_hash.to_hex(), + output_index: utxo_cursor.output_index as u32, + written_block: utxo_cursor.block_number.0, + written_time: blockchain_time, + }, + ) + }, + )); + Ok(conn) + } else { + Err(async_graphql::Error::new("no blockchain")) + } + } +} diff --git a/gql/src/scalars.rs b/gql/src/scalars.rs new file mode 100644 index 0000000000000000000000000000000000000000..f58a79ba540f72b6b54648e70aa92799a1e14cec --- /dev/null +++ b/gql/src/scalars.rs @@ -0,0 +1,83 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use async_graphql::{InputValueError, InputValueResult, Scalar, ScalarType}; +use dubp::crypto::bases::b58::ToBase58; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct PubKeyGva(pub(crate) PublicKey); + +impl async_graphql::Description for PubKeyGva { + fn description() -> &'static str { + "Public key on base 58 representation" + } +} + +#[Scalar(use_type_description = true)] +impl ScalarType for PubKeyGva { + fn parse(value: async_graphql::Value) -> InputValueResult<Self> { + if let async_graphql::Value::String(value_str) = &value { + if value_str.len() < 40 { + Err(InputValueError::custom("too short public key")) + } else if value_str.len() > 44 { + Err(InputValueError::custom("too long public key")) + } else { + Ok(PublicKey::from_base58(value_str).map(PubKeyGva)?) + } + } else { + // If the type does not match + Err(InputValueError::expected_type(value)) + } + } + + fn to_value(&self) -> async_graphql::Value { + async_graphql::Value::String(self.0.to_base58()) + } +} + +pub(crate) struct PkOrScriptGva(pub(crate) WalletScriptV10); + +impl async_graphql::Description for PkOrScriptGva { + fn description() -> &'static str { + "Public key on base 58 representation or complex DUBP script" + } +} + +#[Scalar(use_type_description = true)] +impl ScalarType for PkOrScriptGva { + fn parse(value: async_graphql::Value) -> InputValueResult<Self> { + if let async_graphql::Value::String(value_str) = &value { + Ok(PkOrScriptGva( + if value_str.len() >= 40 || value_str.len() <= 44 { + if let Ok(pubkey) = PublicKey::from_base58(&value_str) { + WalletScriptV10::single_sig(pubkey) + } else { + dubp::documents_parser::wallet_script_from_str(&value_str)? + } + } else { + dubp::documents_parser::wallet_script_from_str(&value_str)? + }, + )) + } else { + // If the type does not match + Err(InputValueError::expected_type(value)) + } + } + + fn to_value(&self) -> async_graphql::Value { + async_graphql::Value::String(self.0.to_string()) + } +} diff --git a/gql/src/schema.rs b/gql/src/schema.rs new file mode 100644 index 0000000000000000000000000000000000000000..c5ab4f6de9ad96279bb7019c3747e68a08a1e44a --- /dev/null +++ b/gql/src/schema.rs @@ -0,0 +1,74 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub type GvaSchema = async_graphql::Schema< + crate::queries::QueryRoot, + crate::mutations::MutationRoot, + crate::subscriptions::SubscriptionRoot, +>; + +pub fn get_schema_definition() -> String { + async_graphql::Schema::build( + queries::QueryRoot::default(), + mutations::MutationRoot::default(), + subscriptions::SubscriptionRoot::default(), + ) + .finish() + .sdl() +} + +pub fn build_schema_with_data(data: GvaSchemaData, logger: bool) -> GvaSchema { + let mut builder = async_graphql::Schema::build( + queries::QueryRoot::default(), + mutations::MutationRoot::default(), + subscriptions::SubscriptionRoot::default(), + ) + .data(data); + if logger { + builder = builder.extension(async_graphql::extensions::Logger); + } + builder.finish() +} + +pub struct GvaSchemaData { + pub cm_accessor: AsyncAccessor, + pub dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + pub dbs_reader: DbsReaderImpl, + pub server_meta_data: ServerMetaData, + pub txs_mempool: TxsMempool, +} + +#[cfg(not(test))] +impl GvaSchemaData { + #[inline(always)] + pub fn cm_accessor(&self) -> AsyncAccessor { + self.cm_accessor + } + #[inline(always)] + pub fn dbs_reader(&self) -> DbsReaderImpl { + self.dbs_reader + } +} +#[cfg(test)] +impl GvaSchemaData { + pub fn cm_accessor(&self) -> AsyncAccessor { + self.cm_accessor.clone() + } + pub fn dbs_reader(&self) -> DbsReaderImpl { + self.dbs_reader.clone() + } +} diff --git a/gql/src/subscriptions.rs b/gql/src/subscriptions.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a5dafaa66de4908d35155e8b553c5709dc79f9e --- /dev/null +++ b/gql/src/subscriptions.rs @@ -0,0 +1,62 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +mod new_blocks; +mod receive_pending_txs; + +use crate::*; +use futures::future::Either; + +#[derive(Clone, Copy, Default, async_graphql::MergedSubscription)] +pub struct SubscriptionRoot( + new_blocks::NewBlocksSubscription, + receive_pending_txs::PendingTxsSubscription, +); + +pub(crate) async fn create_subscription<C, D, E, F, FC, FUT>( + ctx: &async_graphql::Context<'_>, + select_col: FC, + f: F, +) -> impl Stream<Item = async_graphql::Result<D>> +where + C: DbCollectionRo<Event = E, K = E::K, V = E::V>, + E: EventTrait, + F: FnMut(Arc<Events<E>>) -> FUT, + FUT: std::future::Future<Output = Option<async_graphql::Result<D>>>, + FC: 'static + Send + FnOnce(&SharedDbs<FileBackend>) -> &C, +{ + match subscribe_to_col(ctx, select_col).await { + Ok(r) => Either::Left(r.into_stream().filter_map(f)), + Err(e) => { + use futures::FutureExt; + Either::Right(futures::future::ready(Err(e)).into_stream()) + } + } +} + +async fn subscribe_to_col<C, E, F>( + ctx: &async_graphql::Context<'_>, + f: F, +) -> async_graphql::Result<flume::Receiver<Arc<Events<E>>>> +where + C: DbCollectionRo<Event = E, K = E::K, V = E::V>, + E: EventTrait, + F: 'static + Send + FnOnce(&SharedDbs<FileBackend>) -> &C, +{ + let data = ctx.data::<GvaSchemaData>()?; + let (s, r) = flume::unbounded(); + data.dbs_pool.execute(|dbs| f(dbs).subscribe(s)).await??; + Ok(r) +} diff --git a/gql/src/subscriptions/new_blocks.rs b/gql/src/subscriptions/new_blocks.rs new file mode 100644 index 0000000000000000000000000000000000000000..dbbec972b8509509c45db340a54044addcd14e02 --- /dev/null +++ b/gql/src/subscriptions/new_blocks.rs @@ -0,0 +1,51 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use super::create_subscription; +use crate::*; +use duniter_core::dbs::databases::cm_v1::{CmV1DbReadable, CurrentBlockEvent}; + +#[derive(Clone, Copy, Default)] +pub struct NewBlocksSubscription; + +#[async_graphql::Subscription] +impl NewBlocksSubscription { + async fn new_blocks( + &self, + ctx: &async_graphql::Context<'_>, + ) -> impl Stream<Item = async_graphql::Result<Vec<Block>>> { + create_subscription( + ctx, + |dbs| dbs.cm_db.current_block(), + |events| { + let mut blocks = Vec::new(); + for event in events.deref() { + if let CurrentBlockEvent::Upsert { + value: ref block, .. + } = event + { + blocks.push(Block::from(&block.0)); + } + } + if blocks.is_empty() { + futures::future::ready(None) + } else { + futures::future::ready(Some(Ok(blocks))) + } + }, + ) + .await + } +} diff --git a/gql/src/subscriptions/receive_pending_txs.rs b/gql/src/subscriptions/receive_pending_txs.rs new file mode 100644 index 0000000000000000000000000000000000000000..d223f7c65f6daa9d4c8f4e89e6d13f96c5b84a30 --- /dev/null +++ b/gql/src/subscriptions/receive_pending_txs.rs @@ -0,0 +1,52 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use super::create_subscription; +use crate::*; +use duniter_core::dbs::databases::txs_mp_v2::TxsEvent; + +#[derive(Clone, Copy, Default)] +pub struct PendingTxsSubscription; + +#[async_graphql::Subscription] +impl PendingTxsSubscription { + async fn receive_pending_txs( + &self, + ctx: &async_graphql::Context<'_>, + ) -> impl Stream<Item = async_graphql::Result<Vec<TxGva>>> { + create_subscription( + ctx, + |dbs| dbs.txs_mp_db.txs(), + |events| { + let mut txs = Vec::new(); + for event in events.deref() { + if let TxsEvent::Upsert { + value: ref pending_tx, + .. + } = event + { + txs.push(TxGva::from(&pending_tx.0)); + } + } + if txs.is_empty() { + futures::future::ready(None) + } else { + futures::future::ready(Some(Ok(txs))) + } + }, + ) + .await + } +} diff --git a/indexer/Cargo.toml b/indexer/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..59a390a1f1b030ff10f3d34716143e7d04c44cf4 --- /dev/null +++ b/indexer/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "duniter-gva-indexer" +version = "0.1.0" +authors = ["elois <elois@duniter.org>"] +description = "Duniter GVA DB writer" +repository = "https://git.duniter.org/nodes/typescript/duniter" +keywords = ["dubp", "duniter", "blockchain", "database"] +license = "AGPL-3.0" +edition = "2018" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = "1.0.34" +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core" } +duniter-gva-db = { path = "../db" } +dubp = { version = "0.51.0", features = ["duniter"] } +once_cell = "1.5.2" +resiter = "0.4.0" + +[dev-dependencies] +duniter-core = { git = "https://git.duniter.org/nodes/rust/duniter-core", features = ["mem"] } +maplit = "1.0.2" +smallvec = { version = "1.4.0", features = ["serde", "write"] } diff --git a/indexer/src/identities.rs b/indexer/src/identities.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a33b376b0113412977700cc337c5eda5b586b5b --- /dev/null +++ b/indexer/src/identities.rs @@ -0,0 +1,80 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(crate) fn update_identities<B: Backend>( + block: &DubpBlockV10, + identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>, +) -> KvResult<()> { + for mb in block.joiners() { + let pubkey = mb.issuers()[0]; + + let mut idty = identities.get(&PubKeyKeyV2(pubkey))?.unwrap_or_default(); + idty.is_member = true; + idty.joins.push(block.number()); + identities.upsert(PubKeyKeyV2(pubkey), idty); + } + for revo in block.revoked() { + let pubkey = revo.issuer; + if let Some(mut idty) = identities.get(&PubKeyKeyV2(pubkey))? { + idty.is_member = false; + idty.leaves.insert(block.number()); + identities.upsert(PubKeyKeyV2(pubkey), idty) + } + } + for pubkey in block.excluded().iter().copied() { + if let Some(mut idty) = identities.get(&PubKeyKeyV2(pubkey))? { + idty.is_member = false; + idty.leaves.insert(block.number()); + identities.upsert(PubKeyKeyV2(pubkey), idty) + } + } + Ok(()) +} + +pub(crate) fn revert_identities<B: Backend>( + block: &DubpBlockV10, + identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>, +) -> KvResult<()> { + for mb in block.joiners() { + let pubkey = mb.issuers()[0]; + + let mut idty = identities.get(&PubKeyKeyV2(pubkey))?.unwrap_or_default(); + idty.is_member = false; + idty.joins.pop(); + identities.upsert(PubKeyKeyV2(pubkey), idty); + } + for idty in block.identities() { + let pubkey = idty.issuers()[0]; + identities.remove(PubKeyKeyV2(pubkey)); + } + for revo in block.revoked() { + let pubkey = revo.issuer; + if let Some(mut idty) = identities.get(&PubKeyKeyV2(pubkey))? { + idty.is_member = true; + idty.leaves.remove(&block.number()); + identities.upsert(PubKeyKeyV2(pubkey), idty) + } + } + for pubkey in block.excluded().iter().copied() { + if let Some(mut idty) = identities.get(&PubKeyKeyV2(pubkey))? { + idty.is_member = true; + idty.leaves.remove(&block.number()); + identities.upsert(PubKeyKeyV2(pubkey), idty) + } + } + Ok(()) +} diff --git a/indexer/src/lib.rs b/indexer/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..8578580faf16a4a1d3e60e16b3624128207dba88 --- /dev/null +++ b/indexer/src/lib.rs @@ -0,0 +1,593 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +mod identities; +mod tx; +mod utxos; + +use dubp::block::prelude::*; +use dubp::common::crypto::hashs::Hash; +use dubp::common::prelude::*; +use dubp::documents::{ + prelude::*, transaction::TransactionDocumentTrait, transaction::TransactionDocumentV10, +}; +use dubp::wallet::prelude::*; +use duniter_core::dbs::{ + kv_typed::prelude::*, prelude::*, FileBackend, HashKeyV2, PubKeyKeyV2, SourceAmountValV2, + WalletConditionsV2, +}; +use duniter_gva_db::*; +use resiter::filter::Filter; +use std::{ + collections::{BTreeSet, HashMap}, + path::Path, +}; + +static GVA_DB_RO: once_cell::sync::OnceCell<GvaV1DbRo<FileBackend>> = + once_cell::sync::OnceCell::new(); +static GVA_DB_RW: once_cell::sync::OnceCell<GvaV1Db<FileBackend>> = + once_cell::sync::OnceCell::new(); + +pub fn get_gva_db_ro(profile_path_opt: Option<&Path>) -> &'static GvaV1DbRo<FileBackend> { + GVA_DB_RO.get_or_init(|| get_gva_db_rw(profile_path_opt).get_ro_handler()) +} +pub fn get_gva_db_rw(profile_path_opt: Option<&Path>) -> &'static GvaV1Db<FileBackend> { + GVA_DB_RW.get_or_init(|| { + duniter_gva_db::GvaV1Db::<FileBackend>::open(FileBackend::gen_backend_conf( + "gva_v1", + profile_path_opt, + )) + .expect("Fail to open GVA DB") + }) +} + +pub struct UtxoV10<'s> { + pub id: UtxoIdV10, + pub amount: SourceAmount, + pub script: &'s WalletScriptV10, + pub written_block: BlockNumber, +} + +pub fn apply_block<B: Backend>(block: &DubpBlockV10, gva_db: &GvaV1Db<B>) -> KvResult<()> { + let blockstamp = Blockstamp { + number: block.number(), + hash: block.hash(), + }; + gva_db.write(|mut db| { + db.blocks_by_common_time + .upsert(U64BE(block.common_time()), block.number().0); + db.blockchain_time + .upsert(U32BE(block.number().0), block.common_time()); + identities::update_identities::<B>(&block, &mut db.gva_identities)?; + if let Some(divident_amount) = block.dividend() { + db.blocks_with_ud.upsert(U32BE(blockstamp.number.0), ()); + apply_ud::<B>( + blockstamp.number, + divident_amount, + &mut db.balances, + &mut db.gva_identities, + )?; + } + apply_block_txs::<B>( + &mut db, + blockstamp, + block.common_time() as i64, + block.transactions(), + ) + })?; + + Ok(()) +} + +pub fn revert_block<B: Backend>(block: &DubpBlockV10, gva_db: &GvaV1Db<B>) -> KvResult<()> { + gva_db.write(|mut db| { + db.blocks_by_common_time.remove(U64BE(block.common_time())); + db.blockchain_time.remove(U32BE(block.number().0)); + identities::revert_identities::<B>(&block, &mut db.gva_identities)?; + if let Some(divident_amount) = block.dividend() { + db.blocks_with_ud.remove(U32BE(block.number().0)); + revert_ud::<B>( + block.number(), + divident_amount, + &mut db.balances, + &mut db.gva_identities, + )?; + } + + let mut scripts_hash = HashMap::with_capacity(block.transactions().len() * 3); + for tx in block.transactions() { + let tx_hash = tx.get_hash(); + tx::revert_tx::<B>(block.number(), &mut db, &mut scripts_hash, &tx_hash)?.ok_or_else( + || { + KvError::DbCorrupted(format!( + "GVA: tx '{}' dont exist on txs history.", + tx_hash, + )) + }, + )?; + } + db.txs_by_block.remove(U32BE(block.number().0)); + Ok(()) + })?; + + Ok(()) +} + +fn apply_ud<B: Backend>( + block_number: BlockNumber, + divident_amount: SourceAmount, + balances: &mut TxColRw<B::Col, BalancesEvent>, + identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>, +) -> KvResult<()> { + let members = identities.iter(.., |it| { + it.filter_ok(|(_pk, idty)| idty.is_member) + .collect::<KvResult<Vec<_>>>() + })?; + for (pk, mut idty) in members { + if idty.first_ud.is_none() { + idty.first_ud = Some(block_number); + identities.upsert(pk, idty); + } + + // Increase account balance + let account_script = WalletScriptV10::single_sig(pk.0); + let balance = balances + .get(WalletConditionsV2::from_ref(&account_script))? + .unwrap_or_default(); + balances.upsert( + WalletConditionsV2(account_script), + SourceAmountValV2(balance.0 + divident_amount), + ); + } + Ok(()) +} + +fn revert_ud<B: Backend>( + block_number: BlockNumber, + divident_amount: SourceAmount, + balances: &mut TxColRw<B::Col, BalancesEvent>, + identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>, +) -> KvResult<()> { + let members = identities.iter(.., |it| { + it.filter_ok(|(_pk, idty)| idty.is_member) + .collect::<KvResult<Vec<_>>>() + })?; + for (pk, mut idty) in members { + if let Some(first_ud) = idty.first_ud { + if first_ud == block_number { + idty.first_ud = None; + identities.upsert(pk, idty); + } + } + + // Increase account balance + let account_script = WalletScriptV10::single_sig(pk.0); + if let Some(SourceAmountValV2(balance)) = + balances.get(WalletConditionsV2::from_ref(&account_script))? + { + balances.upsert( + WalletConditionsV2(account_script), + SourceAmountValV2(balance - divident_amount), + ); + } + } + Ok(()) +} + +fn apply_block_txs<B: Backend>( + gva_db: &mut GvaV1DbTxRw<B::Col>, + current_blockstamp: Blockstamp, + current_time: i64, + txs: &[TransactionDocumentV10], +) -> KvResult<()> { + let mut scripts_index = HashMap::new(); + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); + let mut txs_hashes = Vec::with_capacity(txs.len()); + for tx in txs { + let tx_hash = tx.get_hash(); + txs_hashes.push(tx_hash); + // Write tx and update sources + tx::apply_tx::<B>( + current_blockstamp, + current_time, + gva_db, + &mut scripts_index, + tx_hash, + tx, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, + )?; + } + + if !txs_hashes.is_empty() { + gva_db + .txs_by_block + .upsert(U32BE(current_blockstamp.number.0), txs_hashes); + } + for (k, v) in txs_by_issuer_mem { + gva_db.txs_by_issuer.upsert(k, v); + } + for (k, v) in txs_by_recipient_mem { + gva_db.txs_by_recipient.upsert(k, v); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use dubp::{ + crypto::keys::{ed25519::PublicKey, PublicKey as _}, + documents::transaction::TransactionDocumentV10Stringified, + documents_parser::prelude::FromStringObject, + }; + + #[test] + fn test_gva_apply_block() -> anyhow::Result<()> { + let gva_db = GvaV1Db::<Mem>::open(MemConf::default())?; + + let s1 = WalletScriptV10::single_sig(PublicKey::from_base58( + "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx", + )?); + let s2 = WalletScriptV10::single_sig(PublicKey::from_base58( + "4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m", + )?); + + let b0 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + version: 10, + median_time: 5_243, + dividend: Some(1000), + joiners: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:elois".to_owned()], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b0, &gva_db)?; + + assert_eq!(gva_db.blocks_by_common_time().count()?, 1); + assert_eq!(gva_db.blocks_by_common_time().get(&U64BE(5_243))?, Some(0)); + assert_eq!(gva_db.blockchain_time().count()?, 1); + assert_eq!(gva_db.blockchain_time().get(&U32BE(0))?, Some(5_243)); + assert_eq!(gva_db.balances().count()?, 1); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(1000))) + ); + assert_eq!(gva_db.txs_by_block().count()?, 0); + + let b1 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + number: 1, + version: 10, + median_time: 5_245, + transactions: vec![TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + locktime: 0, + issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()], + inputs: vec!["1000:0:D:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:0".to_owned()], + unlocks: vec![], + outputs: vec![ + "600:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(), + "400:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(), + ], + comment: "".to_owned(), + signatures: vec![], + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + }], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b1, &gva_db)?; + + assert_eq!(gva_db.blocks_by_common_time().count()?, 2); + assert_eq!(gva_db.blocks_by_common_time().get(&U64BE(5_245))?, Some(1)); + assert_eq!(gva_db.blockchain_time().count()?, 2); + assert_eq!(gva_db.blockchain_time().get(&U32BE(1))?, Some(5_245)); + assert_eq!(gva_db.balances().count()?, 2); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s2.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(600))) + ); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(400))) + ); + assert_eq!(gva_db.gva_utxos().count()?, 2); + assert_eq!( + gva_db + .gva_utxos() + .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?, + vec![ + ( + GvaUtxoIdDbV1::new(s1.clone(), 1, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(400)) + ), + ( + GvaUtxoIdDbV1::new(s2.clone(), 1, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(600)) + ), + ] + ); + assert_eq!(gva_db.txs_by_block().count()?, 1); + assert_eq!( + gva_db.txs_by_block().get(&U32BE(1))?, + Some(vec![Hash::from_hex( + "0000000000000000000000000000000000000000000000000000000000000000" + )?]) + ); + + let b2 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + number: 2, + version: 10, + median_time: 5_247, + transactions: vec![TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + locktime: 0, + issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()], + inputs: vec!["400:0:T:0000000000000000000000000000000000000000000000000000000000000000:1".to_owned()], + unlocks: vec![], + outputs: vec![ + "300:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(), + "100:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(), + ], + comment: "".to_owned(), + signatures: vec![], + hash: Some("0101010101010101010101010101010101010101010101010101010101010101".to_owned()), + }], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b2, &gva_db)?; + + assert_eq!(gva_db.blocks_by_common_time().count()?, 3); + assert_eq!(gva_db.blocks_by_common_time().get(&U64BE(5_247))?, Some(2)); + assert_eq!(gva_db.blockchain_time().count()?, 3); + assert_eq!(gva_db.blockchain_time().get(&U32BE(2))?, Some(5_247)); + assert_eq!(gva_db.balances().count()?, 2); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s2.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(700))) + ); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(300))) + ); + assert_eq!(gva_db.gva_utxos().count()?, 3); + assert_eq!( + gva_db + .gva_utxos() + .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?, + vec![ + ( + GvaUtxoIdDbV1::new(s1, 2, Hash([1; 32]), 0), + SourceAmountValV2(SourceAmount::with_base0(300)) + ), + ( + GvaUtxoIdDbV1::new(s2.clone(), 1, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(600)) + ), + ( + GvaUtxoIdDbV1::new(s2, 2, Hash([1; 32]), 1), + SourceAmountValV2(SourceAmount::with_base0(100)) + ), + ] + ); + assert_eq!(gva_db.txs_by_block().count()?, 2); + assert_eq!( + gva_db.txs_by_block().get(&U32BE(2))?, + Some(vec![Hash::from_hex( + "0101010101010101010101010101010101010101010101010101010101010101" + )?]) + ); + + Ok(()) + } + + #[test] + fn test_gva_revert_block() -> anyhow::Result<()> { + let gva_db = GvaV1Db::<Mem>::open(MemConf::default())?; + + let s1 = WalletScriptV10::single_sig(PublicKey::from_base58( + "D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx", + )?); + let s2 = WalletScriptV10::single_sig(PublicKey::from_base58( + "4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m", + )?); + + let b0 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + version: 10, + median_time: 5_243, + dividend: Some(1000), + joiners: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:FFeyrvYio9uYwY5aMcDGswZPNjGLrl8THn9l3EPKSNySD3SDSHjCljSfFEwb87sroyzJQoVzPwER0sW/cbZMDg==:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:0-E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855:elois".to_owned()], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b0, &gva_db)?; + + let b1 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + number: 1, + version: 10, + median_time: 5_245, + transactions: vec![TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + locktime: 0, + issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()], + inputs: vec!["1000:0:D:D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx:0".to_owned()], + unlocks: vec![], + outputs: vec![ + "600:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(), + "400:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(), + ], + comment: "".to_owned(), + signatures: vec![], + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + }], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b1, &gva_db)?; + + let b2 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + number: 2, + version: 10, + median_time: 5_247, + transactions: vec![TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + locktime: 0, + issuers: vec!["D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx".to_owned()], + inputs: vec!["400:0:T:0000000000000000000000000000000000000000000000000000000000000000:1".to_owned()], + unlocks: vec![], + outputs: vec![ + "400:0:SIG(4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m)".to_owned(), + ], + comment: "".to_owned(), + signatures: vec![], + hash: Some("0101010101010101010101010101010101010101010101010101010101010101".to_owned()), + }], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b2, &gva_db)?; + + let b3 = DubpBlockV10::from_string_object(&DubpBlockV10Stringified { + number: 3, + version: 10, + median_time: 5_249, + transactions: vec![TransactionDocumentV10Stringified { + currency: "test".to_owned(), + blockstamp: "0-0000000000000000000000000000000000000000000000000000000000000000".to_owned(), + locktime: 0, + issuers: vec!["4fHMTFBMo5sTQEc5p1CNWz28S4mnnqdUBmECq1zt4n2m".to_owned()], + inputs: vec!["400:0:T:0101010101010101010101010101010101010101010101010101010101010101:0".to_owned()], + unlocks: vec![], + outputs: vec![ + "400:0:SIG(D9D2zaJoWYWveii1JRYLVK3J4Z7ZH3QczoKrnQeiM6mx)".to_owned(), + ], + comment: "".to_owned(), + signatures: vec![], + hash: Some("0202020202020202020202020202020202020202020202020202020202020202".to_owned()), + }], + inner_hash: Some("0000000A65A12DB95B3153BCD05DB4D5C30CC7F0B1292D9FFBC3DE67F72F6040".to_owned()), + signature: "7B0hvcfajE2G8nBLp0vLVaQcQdQIyli21Gu8F2l+nimKHRe+fUNi+MWd1e/u29BYZa+RZ1yxhbHIbFzytg7fAA==".to_owned(), + hash: Some("0000000000000000000000000000000000000000000000000000000000000000".to_owned()), + ..Default::default() + })?; + + apply_block(&b3, &gva_db)?; + + revert_block(&b3, &gva_db)?; + + assert_eq!(gva_db.blockchain_time().count()?, 3); + assert_eq!(gva_db.blockchain_time().get(&U32BE(2))?, Some(5_247)); + assert_eq!(gva_db.balances().count()?, 2); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1.clone()))?, + Some(SourceAmountValV2(SourceAmount::ZERO)) + ); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s2.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(1_000))) + ); + assert_eq!(gva_db.gva_utxos().count()?, 2); + assert_eq!( + gva_db + .gva_utxos() + .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?, + vec![ + ( + GvaUtxoIdDbV1::new(s2.clone(), 1, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(600)) + ), + ( + GvaUtxoIdDbV1::new(s2.clone(), 2, Hash([1u8; 32]), 0), + SourceAmountValV2(SourceAmount::with_base0(400)) + ), + ] + ); + + revert_block(&b2, &gva_db)?; + + assert_eq!(gva_db.blockchain_time().count()?, 2); + assert_eq!(gva_db.blockchain_time().get(&U32BE(1))?, Some(5_245)); + assert_eq!(gva_db.balances().count()?, 2); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s2.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(600))) + ); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1.clone()))?, + Some(SourceAmountValV2(SourceAmount::with_base0(400))) + ); + assert_eq!(gva_db.gva_utxos().count()?, 2); + assert_eq!( + gva_db + .gva_utxos() + .iter(.., |it| it.collect::<KvResult<Vec<_>>>())?, + vec![ + ( + GvaUtxoIdDbV1::new(s1.clone(), 1, Hash::default(), 1), + SourceAmountValV2(SourceAmount::with_base0(400)) + ), + ( + GvaUtxoIdDbV1::new(s2.clone(), 1, Hash::default(), 0), + SourceAmountValV2(SourceAmount::with_base0(600)) + ), + ] + ); + + revert_block(&b1, &gva_db)?; + + assert_eq!(gva_db.blockchain_time().count()?, 1); + assert_eq!(gva_db.blockchain_time().get(&U32BE(0))?, Some(5_243)); + assert_eq!(gva_db.balances().count()?, 1); + assert_eq!( + gva_db.balances().get(&WalletConditionsV2(s1))?, + Some(SourceAmountValV2(SourceAmount::with_base0(1000))) + ); + assert_eq!(gva_db.balances().get(&WalletConditionsV2(s2))?, None); + + Ok(()) + } +} diff --git a/indexer/src/tx.rs b/indexer/src/tx.rs new file mode 100644 index 0000000000000000000000000000000000000000..627d8cb2191423a781828d573c73a8183a138e08 --- /dev/null +++ b/indexer/src/tx.rs @@ -0,0 +1,514 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(crate) type ScriptsHash = HashMap<WalletScriptV10, Hash>; + +fn get_script_hash(script: &WalletScriptV10, scripts_hash: &mut ScriptsHash) -> Hash { + if let Some(script_hash) = scripts_hash.get(script) { + *script_hash + } else { + let script_hash = Hash::compute(script.to_string().as_bytes()); + scripts_hash.insert(script.clone(), script_hash); + script_hash + } +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn apply_tx<B: Backend>( + current_blockstamp: Blockstamp, + current_time: i64, + gva_db: &mut GvaV1DbTxRw<B::Col>, + scripts_hash: &mut ScriptsHash, + tx_hash: Hash, + tx: &TransactionDocumentV10, + txs_by_issuer_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>, + txs_by_recipient_mem: &mut HashMap<WalletHashWithBnV1Db, BTreeSet<Hash>>, +) -> KvResult<()> { + let mut issuers_scripts_hashs = BTreeSet::new(); + for input in tx.get_inputs() { + let (account_script_hash, account_script) = match input.id { + SourceIdV10::Utxo(utxo_id) => { + // Get issuer script & written block + let db_tx_origin = gva_db + .txs + .get(&HashKeyV2::from_ref(&utxo_id.tx_hash))? + .ok_or_else(|| { + KvError::DbCorrupted(format!("Not found origin tx of uxto {}", utxo_id)) + })?; + let utxo_script = db_tx_origin.tx.get_outputs()[utxo_id.output_index] + .conditions + .script + .clone(); + let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash); + + // Remove consumed UTXOs + super::utxos::remove_utxo_v10::<B>( + &mut gva_db.scripts_by_pubkey, + &mut gva_db.gva_utxos, + utxo_id, + &utxo_script, + utxo_script_hash, + db_tx_origin.written_block.number.0, + )?; + + // Return utxo_script with hash + (utxo_script_hash, utxo_script) + } + SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => { + let script = WalletScriptV10::single_sig(issuer); + (Hash::compute(script.to_string().as_bytes()), script) + } + }; + issuers_scripts_hashs.insert(account_script_hash); + // Insert on col `txs_by_issuer` + txs_by_issuer_mem + .entry(WalletHashWithBnV1Db::new( + account_script_hash, + current_blockstamp.number, + )) + .or_default() + .insert(tx_hash); + // Decrease account balance + decrease_account_balance::<B>( + account_script, + account_script_hash, + &mut gva_db.balances, + input.amount, + &mut gva_db.gva_identities, + false, + &mut gva_db.txs_by_recipient, + )?; + } + + for (output_index, output) in tx.get_outputs().iter().enumerate() { + let utxo_script_hash = get_script_hash(&output.conditions.script, scripts_hash); + // Insert created UTXOs + super::utxos::write_utxo_v10::<B>( + &mut gva_db.scripts_by_pubkey, + &mut gva_db.gva_utxos, + UtxoV10 { + id: UtxoIdV10 { + tx_hash, + output_index, + }, + amount: output.amount, + script: &output.conditions.script, + written_block: current_blockstamp.number, + }, + utxo_script_hash, + )?; + + // Insert on col `txs_by_recipient` + if !issuers_scripts_hashs.contains(&utxo_script_hash) { + txs_by_recipient_mem + .entry(WalletHashWithBnV1Db::new( + utxo_script_hash, + current_blockstamp.number, + )) + .or_default() + .insert(tx_hash); + } + + // Increase account balance + let balance = gva_db + .balances + .get(WalletConditionsV2::from_ref(&output.conditions.script))? + .unwrap_or_default(); + gva_db.balances.upsert( + WalletConditionsV2(output.conditions.script.clone()), + SourceAmountValV2(balance.0 + output.amount), + ); + } + + // Insert tx itself + gva_db.txs.upsert( + HashKeyV2(tx_hash), + GvaTxDbV1 { + tx: tx.clone(), + written_block: current_blockstamp, + written_time: current_time, + }, + ); + + Ok(()) +} + +pub(crate) fn revert_tx<B: Backend>( + block_number: BlockNumber, + gva_db: &mut GvaV1DbTxRw<B::Col>, + scripts_hash: &mut ScriptsHash, + tx_hash: &Hash, +) -> KvResult<Option<TransactionDocumentV10>> { + if let Some(tx_db) = gva_db.txs.get(&HashKeyV2::from_ref(tx_hash))? { + use dubp::documents::transaction::TransactionDocumentTrait as _; + for (output_index, output) in tx_db.tx.get_outputs().iter().enumerate() { + let script = &output.conditions.script; + let utxo_script_hash = get_script_hash(&script, scripts_hash); + + // Remove UTXOs created by this tx + super::utxos::remove_utxo_v10::<B>( + &mut gva_db.scripts_by_pubkey, + &mut gva_db.gva_utxos, + UtxoIdV10 { + tx_hash: *tx_hash, + output_index, + }, + script, + utxo_script_hash, + block_number.0, + )?; + + // Remove on col `txs_by_recipient` + let k = WalletHashWithBnV1Db::new(utxo_script_hash, block_number); + gva_db.txs_by_recipient.remove(k); + + // Decrease account balance + decrease_account_balance::<B>( + script.clone(), + utxo_script_hash, + &mut gva_db.balances, + output.amount, + &mut gva_db.gva_identities, + true, + &mut gva_db.txs_by_recipient, + )?; + } + // Recreate UTXOs consumed by this tx (and update balance) + for input in tx_db.tx.get_inputs() { + let (account_script_hash, account_script) = match input.id { + SourceIdV10::Utxo(utxo_id) => { + let db_tx_origin = gva_db + .txs + .get(&HashKeyV2::from_ref(&utxo_id.tx_hash))? + .ok_or_else(|| { + KvError::DbCorrupted(format!("Not found origin tx of uxto {}", utxo_id)) + })?; + let utxo_script = db_tx_origin.tx.get_outputs()[utxo_id.output_index] + .conditions + .script + .clone(); + let utxo_script_hash = get_script_hash(&utxo_script, scripts_hash); + super::utxos::write_utxo_v10::<B>( + &mut gva_db.scripts_by_pubkey, + &mut gva_db.gva_utxos, + UtxoV10 { + id: utxo_id, + amount: input.amount, + script: &utxo_script, + written_block: db_tx_origin.written_block.number, + }, + utxo_script_hash, + )?; + + // Return utxo_script + (utxo_script_hash, utxo_script) + } + SourceIdV10::Ud(UdSourceIdV10 { issuer, .. }) => { + let script = WalletScriptV10::single_sig(issuer); + (Hash::compute(script.to_string().as_bytes()), script) + } + }; + // Remove on col `txs_by_issuer` + gva_db + .txs_by_issuer + .remove(WalletHashWithBnV1Db::new(account_script_hash, block_number)); + // Increase account balance + let balance = gva_db + .balances + .get(WalletConditionsV2::from_ref(&account_script))? + .unwrap_or_default(); + + gva_db.balances.upsert( + WalletConditionsV2(account_script), + SourceAmountValV2(balance.0 + input.amount), + ); + } + + // Remove tx itself + gva_db.txs.remove(HashKeyV2(*tx_hash)); + + Ok(Some(tx_db.tx)) + } else { + Ok(None) + } +} + +fn decrease_account_balance<B: Backend>( + account_script: WalletScriptV10, + account_script_hash: Hash, + balances: &mut TxColRw<B::Col, BalancesEvent>, + decrease_amount: SourceAmount, + identities: &mut TxColRw<B::Col, GvaIdentitiesEvent>, + revert: bool, + txs_by_recipients: &mut TxColRw<B::Col, TxsByRecipientEvent>, +) -> KvResult<()> { + if let Some(SourceAmountValV2(balance)) = + balances.get(WalletConditionsV2::from_ref(&account_script))? + { + let new_balance = balance - decrease_amount; + let remove_balance = if revert && new_balance == SourceAmount::ZERO { + let (k_min, k_max) = WalletHashWithBnV1Db::wallet_hash_interval(account_script_hash); + if txs_by_recipients + .iter(k_min..k_max, |it| it.keys().next_res())? + .is_some() + { + false + } else if let Some(pubkey) = account_script.as_single_sig() { + if let Some(idty) = identities.get(&PubKeyKeyV2(pubkey))? { + idty.first_ud.is_none() + } else { + true + } + } else { + true + } + } else { + false + }; + if remove_balance { + balances.remove(WalletConditionsV2(account_script)); + } else { + balances.upsert( + WalletConditionsV2(account_script), + SourceAmountValV2(new_balance), + ); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use dubp::{ + crypto::keys::ed25519::Ed25519KeyPair, crypto::keys::KeyPair as _, + documents::smallvec::smallvec as svec, documents::transaction::v10::*, + documents::transaction::UTXOConditions, + }; + use duniter_core::dbs::BlockMetaV2; + use maplit::btreeset; + + #[test] + fn test_apply_tx() -> KvResult<()> { + let kp = Ed25519KeyPair::generate_random().expect("gen rand kp"); + let kp2 = Ed25519KeyPair::generate_random().expect("gen rand kp"); + + let ud0_amount = SourceAmount::with_base0(1000); + let o1_amount = ud0_amount - SourceAmount::with_base0(600); + let o2_amount = ud0_amount - SourceAmount::with_base0(400); + + let gva_db = GvaV1Db::<Mem>::open(MemConf::default())?; + + let b0 = BlockMetaV2 { + dividend: Some(ud0_amount), + ..Default::default() + }; + let current_blockstamp = b0.blockstamp(); + let pk = kp.public_key(); + //println!("TMP pk1={}", pk); + let pk2 = kp2.public_key(); + //println!("TMP pk2={}", pk2); + let script = WalletScriptV10::single_sig(pk); + let script2 = WalletScriptV10::single_sig(pk2); + let script_hash = Hash::compute(script.to_string().as_bytes()); + let script2_hash = Hash::compute(script2.to_string().as_bytes()); + + gva_db.balances_write().upsert( + WalletConditionsV2(script.clone()), + SourceAmountValV2(ud0_amount), + )?; + + let tx1 = TransactionDocumentV10Builder { + currency: "test", + blockstamp: current_blockstamp, + locktime: 0, + issuers: svec![pk], + inputs: &[TransactionInputV10 { + amount: ud0_amount, + id: SourceIdV10::Ud(UdSourceIdV10 { + issuer: pk, + block_number: BlockNumber(0), + }), + }], + unlocks: &[TransactionInputUnlocksV10::default()], + outputs: svec![ + TransactionOutputV10 { + amount: o1_amount, + conditions: UTXOConditions::from(script2.clone()), + }, + TransactionOutputV10 { + amount: o2_amount, + conditions: UTXOConditions::from(script.clone()), + } + ], + comment: "", + hash: None, + } + .build_and_sign(vec![kp.generate_signator()]); + let tx1_hash = tx1.get_hash(); + + let mut scripts_hash = HashMap::new(); + + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); + (&gva_db).write(|mut db| { + apply_tx::<Mem>( + current_blockstamp, + b0.median_time as i64, + &mut db, + &mut scripts_hash, + tx1_hash, + &tx1, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, + ) + })?; + + assert_eq!(txs_by_issuer_mem.len(), 1); + assert_eq!( + txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))), + Some(&btreeset![tx1_hash]) + ); + assert_eq!(txs_by_recipient_mem.len(), 1); + assert_eq!( + txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))), + Some(&btreeset![tx1_hash]) + ); + + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script2))?, + Some(SourceAmountValV2(o1_amount)) + ); + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script))?, + Some(SourceAmountValV2(o2_amount)) + ); + + let tx2 = TransactionDocumentV10Builder { + currency: "test", + blockstamp: current_blockstamp, + locktime: 0, + issuers: svec![pk2], + inputs: &[TransactionInputV10 { + amount: o1_amount, + id: SourceIdV10::Utxo(UtxoIdV10 { + tx_hash: tx1_hash, + output_index: 0, + }), + }], + unlocks: &[TransactionInputUnlocksV10::default()], + outputs: svec![TransactionOutputV10 { + amount: o1_amount, + conditions: UTXOConditions::from(script.clone()), + },], + comment: "", + hash: None, + } + .build_and_sign(vec![kp.generate_signator()]); + let tx2_hash = tx2.get_hash(); + + let mut txs_by_issuer_mem = HashMap::new(); + let mut txs_by_recipient_mem = HashMap::new(); + (&gva_db).write(|mut db| { + apply_tx::<Mem>( + current_blockstamp, + b0.median_time as i64, + &mut db, + &mut scripts_hash, + tx2_hash, + &tx2, + &mut txs_by_issuer_mem, + &mut txs_by_recipient_mem, + ) + })?; + + assert_eq!(txs_by_issuer_mem.len(), 1); + assert_eq!( + txs_by_issuer_mem.get(&WalletHashWithBnV1Db::new(script2_hash, BlockNumber(0))), + Some(&btreeset![tx2_hash]) + ); + assert_eq!(txs_by_recipient_mem.len(), 1); + assert_eq!( + txs_by_recipient_mem.get(&WalletHashWithBnV1Db::new(script_hash, BlockNumber(0))), + Some(&btreeset![tx2_hash]) + ); + + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script2))?, + Some(SourceAmountValV2(SourceAmount::ZERO)) + ); + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script))?, + Some(SourceAmountValV2(ud0_amount)) + ); + + (&gva_db).write(|mut db| { + revert_tx::<Mem>( + current_blockstamp.number, + &mut db, + &mut scripts_hash, + &tx2_hash, + ) + })?; + + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script2))?, + Some(SourceAmountValV2(o1_amount)) + ); + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script))?, + Some(SourceAmountValV2(o2_amount)) + ); + + (&gva_db).write(|mut db| { + revert_tx::<Mem>( + current_blockstamp.number, + &mut db, + &mut scripts_hash, + &tx1_hash, + ) + })?; + + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script2))?, + None + ); + assert_eq!( + gva_db + .balances() + .get(WalletConditionsV2::from_ref(&script))?, + Some(SourceAmountValV2(ud0_amount)) + ); + + Ok(()) + } +} diff --git a/indexer/src/utxos.rs b/indexer/src/utxos.rs new file mode 100644 index 0000000000000000000000000000000000000000..91ec82723ebb0c51605b05cd710739df339a15fd --- /dev/null +++ b/indexer/src/utxos.rs @@ -0,0 +1,86 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +pub(crate) fn write_utxo_v10<'s, B: Backend>( + scripts_by_pubkey: &mut TxColRw<B::Col, ScriptsByPubkeyEvent>, + gva_utxos: &mut TxColRw<B::Col, GvaUtxosEvent>, + utxo: UtxoV10<'s>, + utxo_script_hash: Hash, +) -> KvResult<()> { + for pubkey in utxo.script.pubkeys() { + let mut pubkey_scripts = scripts_by_pubkey + .get(&PubKeyKeyV2(pubkey))? + .unwrap_or_default(); + if !pubkey_scripts.0.contains(&utxo.script) { + pubkey_scripts.0.insert(utxo.script.clone()); + scripts_by_pubkey.upsert(PubKeyKeyV2(pubkey), pubkey_scripts); + } + } + + let block_number = utxo.written_block.0; + let utxo_amount = utxo.amount; + let utxo_id = utxo.id; + gva_utxos.upsert( + GvaUtxoIdDbV1::new_( + utxo_script_hash, + block_number, + utxo_id.tx_hash, + utxo_id.output_index as u8, + ), + SourceAmountValV2(utxo_amount), + ); + + Ok(()) +} + +pub(crate) fn remove_utxo_v10<B: Backend>( + scripts_by_pubkey: &mut TxColRw<B::Col, ScriptsByPubkeyEvent>, + gva_utxos: &mut TxColRw<B::Col, GvaUtxosEvent>, + utxo_id: UtxoIdV10, + utxo_script: &WalletScriptV10, + utxo_script_hash: Hash, + written_block_number: u32, +) -> KvResult<()> { + gva_utxos.remove(GvaUtxoIdDbV1::new_( + utxo_script_hash, + written_block_number, + utxo_id.tx_hash, + utxo_id.output_index as u8, + )); + + let (k_min, k_max) = GvaUtxoIdDbV1::script_interval(utxo_script_hash); + if gva_utxos + .iter(k_min..k_max, |it| it.keys().next_res())? + .is_none() + { + let pubkeys = utxo_script.pubkeys(); + for pubkey in pubkeys { + let mut pubkey_scripts = + scripts_by_pubkey + .get(&PubKeyKeyV2(pubkey))? + .ok_or_else(|| { + KvError::DbCorrupted(format!( + "GVA: key {} dont exist on col `scripts_by_pubkey`.", + pubkey, + )) + })?; + pubkey_scripts.0.remove(utxo_script); + scripts_by_pubkey.upsert(PubKeyKeyV2(pubkey), pubkey_scripts); + } + } + Ok(()) +} diff --git a/src/anti_spam.rs b/src/anti_spam.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7609d1740f062d6dcf32608ab822c22b83ba290 --- /dev/null +++ b/src/anti_spam.rs @@ -0,0 +1,201 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; +use async_mutex::Mutex; +use duniter_core::dbs::kv_typed::prelude::Arc; +use std::{ + collections::{HashMap, HashSet}, + net::IpAddr, + time::Duration, + time::Instant, +}; + +pub(super) const MAX_BATCH_SIZE: usize = 5; + +const COUNT_INTERVAL: usize = 10; +const MIN_DURATION_INTERVAL: Duration = Duration::from_secs(20); +const LARGE_DURATION_INTERVAL: Duration = Duration::from_secs(180); +const REDUCED_COUNT_INTERVAL: usize = COUNT_INTERVAL / 2; +const MAX_BAN_COUNT: usize = 16; +const BAN_FORGET_MIN_DURATION: Duration = Duration::from_secs(180); + +#[derive(Clone)] +pub(crate) struct AntiSpam { + state: Arc<Mutex<AntiSpamInner>>, + whitelist: HashSet<IpAddr>, +} + +#[derive(Clone)] +pub(crate) struct AntiSpamResponse { + pub is_whitelisted: bool, + pub is_ok: bool, +} + +impl AntiSpamResponse { + fn ban() -> Self { + AntiSpamResponse { + is_whitelisted: false, + is_ok: false, + } + } + fn ok() -> Self { + AntiSpamResponse { + is_whitelisted: false, + is_ok: true, + } + } + fn whitelisted() -> Self { + AntiSpamResponse { + is_whitelisted: true, + is_ok: true, + } + } +} + +struct AntiSpamInner { + ban: HashMap<IpAddr, (bool, usize, Instant)>, + ips_time: HashMap<IpAddr, (usize, Instant)>, +} + +impl From<&GvaConf> for AntiSpam { + fn from(conf: &GvaConf) -> Self { + AntiSpam { + state: Arc::new(Mutex::new(AntiSpamInner { + ban: HashMap::with_capacity(10), + ips_time: HashMap::with_capacity(10), + })), + whitelist: conf.get_whitelist().iter().copied().collect(), + } + } +} + +impl AntiSpam { + pub(crate) async fn verify( + &self, + remote_addr_opt: Option<std::net::IpAddr>, + ) -> AntiSpamResponse { + if let Some(ip) = remote_addr_opt { + log::trace!("GVA: receive request from {}", ip); + if self.whitelist.contains(&ip) { + AntiSpamResponse::whitelisted() + } else { + let mut guard = self.state.lock().await; + if let Some((is_banned, ban_count, instant)) = guard.ban.get(&ip).copied() { + let ban_duration = + Duration::from_secs(1 << std::cmp::min(ban_count, MAX_BAN_COUNT)); + if is_banned { + if Instant::now().duration_since(instant) > ban_duration { + guard.ban.insert(ip, (false, ban_count + 1, Instant::now())); + guard.ips_time.insert(ip, (1, Instant::now())); + AntiSpamResponse::ok() + } else { + guard.ban.insert(ip, (true, ban_count + 1, Instant::now())); + AntiSpamResponse::ban() + } + } else if Instant::now().duration_since(instant) + > std::cmp::max(ban_duration, BAN_FORGET_MIN_DURATION) + { + guard.ban.remove(&ip); + guard.ips_time.insert(ip, (1, Instant::now())); + AntiSpamResponse::ok() + } else { + Self::verify_interval(ip, &mut guard, ban_count) + } + } else { + Self::verify_interval(ip, &mut guard, 0) + } + } + } else { + AntiSpamResponse::ban() + } + } + fn verify_interval( + ip: IpAddr, + state: &mut AntiSpamInner, + ban_count: usize, + ) -> AntiSpamResponse { + if let Some((count, instant)) = state.ips_time.get(&ip).copied() { + if count == COUNT_INTERVAL { + let duration = Instant::now().duration_since(instant); + if duration > MIN_DURATION_INTERVAL { + if duration > LARGE_DURATION_INTERVAL { + state.ips_time.insert(ip, (1, Instant::now())); + AntiSpamResponse::ok() + } else { + state + .ips_time + .insert(ip, (REDUCED_COUNT_INTERVAL, Instant::now())); + AntiSpamResponse::ok() + } + } else { + state.ban.insert(ip, (true, ban_count, Instant::now())); + AntiSpamResponse::ban() + } + } else { + state.ips_time.insert(ip, (count + 1, instant)); + AntiSpamResponse::ok() + } + } else { + state.ips_time.insert(ip, (1, Instant::now())); + AntiSpamResponse::ok() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::{Ipv4Addr, Ipv6Addr}; + + const LOCAL_IP4: IpAddr = IpAddr::V4(Ipv4Addr::LOCALHOST); + const LOCAL_IP6: IpAddr = IpAddr::V6(Ipv6Addr::LOCALHOST); + + #[tokio::test] + async fn test_anti_spam() { + let anti_spam = AntiSpam::from(&GvaConf::default()); + assert!(!anti_spam.verify(None).await.is_ok); + + for _ in 0..(COUNT_INTERVAL * 2) { + assert!(anti_spam.verify(Some(LOCAL_IP4)).await.is_ok); + assert!(anti_spam.verify(Some(LOCAL_IP6)).await.is_ok); + } + + let extern_ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED); + + // Consume max queries + for _ in 0..COUNT_INTERVAL { + assert!(anti_spam.verify(Some(extern_ip)).await.is_ok); + } + // Should be banned + assert!(!anti_spam.verify(Some(extern_ip)).await.is_ok); + + // Should be un-banned after one second + tokio::time::sleep(Duration::from_millis(1_100)).await; + // Re-consume max queries + for _ in 0..COUNT_INTERVAL { + assert!(anti_spam.verify(Some(extern_ip)).await.is_ok); + } + // Should be banned for 2 seconds this time + tokio::time::sleep(Duration::from_millis(1_100)).await; + // Attempting a request when I'm banned must be twice my banning time + assert!(!anti_spam.verify(Some(extern_ip)).await.is_ok); + tokio::time::sleep(Duration::from_millis(4_100)).await; + // Re-consume max queries + for _ in 0..COUNT_INTERVAL { + assert!(anti_spam.verify(Some(extern_ip)).await.is_ok); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..2c560453bbce36eb242766a58ef1e184a75b345f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,371 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![deny( + clippy::unwrap_used, + missing_copy_implementations, + trivial_casts, + trivial_numeric_casts, + unstable_features, + unused_import_braces +)] + +mod anti_spam; +mod warp_; + +pub use duniter_core::conf::gva_conf::GvaConf; + +use async_graphql::http::GraphQLPlaygroundConfig; +use dubp::common::prelude::*; +use dubp::documents::transaction::TransactionDocumentV10; +use dubp::{block::DubpBlockV10, crypto::hashs::Hash}; +use dubp::{ + common::crypto::keys::{ed25519::PublicKey, KeyPair as _}, + crypto::keys::ed25519::Ed25519KeyPair, +}; +use duniter_core::conf::DuniterMode; +use duniter_core::dbs::databases::txs_mp_v2::TxsMpV2DbReadable; +use duniter_core::dbs::prelude::*; +use duniter_core::dbs::{kv_typed::prelude::*, FileBackend}; +use duniter_core::global::AsyncAccessor; +use duniter_core::mempools::Mempools; +use duniter_gva_db::*; +use duniter_gva_gql::{GvaSchema, QueryContext}; +use duniter_gva_indexer::{get_gva_db_ro, get_gva_db_rw}; +use futures::{StreamExt, TryStreamExt}; +use std::{convert::Infallible, path::Path}; +use warp::{http::Response as HttpResponse, Filter as _, Rejection}; + +#[derive(Debug)] +pub struct GvaModule { + conf: Option<GvaConf>, + currency: String, + dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + gva_db_ro: &'static GvaV1DbRo<FileBackend>, + mempools: Mempools, + mode: DuniterMode, + self_keypair: Ed25519KeyPair, + software_version: &'static str, +} + +#[async_trait::async_trait] +impl duniter_core::module::DuniterModule for GvaModule { + const INDEX_BLOCKS: bool = true; + + fn apply_block( + block: &DubpBlockV10, + _conf: &duniter_core::conf::DuniterConf, + profile_path_opt: Option<&Path>, + ) -> KvResult<()> { + let gva_db = get_gva_db_rw(profile_path_opt); + duniter_gva_indexer::apply_block(&block, gva_db) + } + fn revert_block( + block: &DubpBlockV10, + _conf: &duniter_core::conf::DuniterConf, + profile_path_opt: Option<&Path>, + ) -> KvResult<()> { + let gva_db = get_gva_db_rw(profile_path_opt); + duniter_gva_indexer::revert_block(&block, gva_db) + } + fn init( + conf: &duniter_core::conf::DuniterConf, + currency: &str, + dbs_pool: &fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + mempools: Mempools, + mode: duniter_core::conf::DuniterMode, + profile_path_opt: Option<&Path>, + software_version: &'static str, + ) -> anyhow::Result<(Self, Vec<duniter_core::module::Endpoint>)> { + let mut endpoints = Vec::new(); + if let Some(conf) = conf.gva.clone() { + let remote_port = conf.get_remote_port(); + endpoints.push(format!( + "GVA {}{} {} {}", + if remote_port == 443 || conf.get_remote_tls() { + "S " + } else { + "" + }, + conf.get_remote_host(), + remote_port, + conf.get_remote_path(), + )); + endpoints.push(format!( + "GVASUB {}{} {} {}", + if remote_port == 443 || conf.get_remote_tls() { + "S " + } else { + "" + }, + conf.get_remote_host(), + remote_port, + conf.get_remote_subscriptions_path(), + )); + }; + Ok(( + GvaModule { + conf: conf.gva.to_owned(), + currency: currency.to_owned(), + dbs_pool: dbs_pool.to_owned(), + gva_db_ro: get_gva_db_ro(profile_path_opt), + mempools, + mode, + self_keypair: conf.self_key_pair.clone(), + software_version, + }, + endpoints, + )) + } + + async fn start(self) -> anyhow::Result<()> { + // Do not start GVA server on js tests + if std::env::var_os("DUNITER_JS_TESTS") != Some("yes".into()) { + let GvaModule { + conf, + currency, + dbs_pool, + gva_db_ro, + mempools, + mode, + self_keypair, + software_version, + } = self; + + if let DuniterMode::Start = mode { + if let Some(conf) = conf { + GvaModule::start_inner( + conf, + currency, + dbs_pool, + gva_db_ro, + mempools, + self_keypair, + software_version, + ) + .await + } + } + } + Ok(()) + } + // Needed for BMA only, will be removed when the migration is complete. + fn get_transactions_history_for_bma( + dbs_pool: &fast_threadpool::ThreadPoolSyncHandler<SharedDbs<FileBackend>>, + profile_path_opt: Option<&Path>, + pubkey: PublicKey, + ) -> KvResult<Option<duniter_core::module::TxsHistoryForBma>> { + let gva_db = get_gva_db_ro(profile_path_opt); + let duniter_gva_dbs_reader::txs_history::TxsHistory { + sent, + received, + sending, + pending, + } = dbs_pool + .execute(move |dbs| { + duniter_gva_dbs_reader::txs_history::get_transactions_history_for_bma( + gva_db, + &dbs.txs_mp_db, + pubkey, + ) + }) + .expect("dbs pool disconnected")?; + Ok(Some(duniter_core::module::TxsHistoryForBma { + sent: sent + .into_iter() + .map( + |GvaTxDbV1 { + tx, + written_block, + written_time, + }| (tx, written_block, written_time), + ) + .collect(), + received: received + .into_iter() + .map( + |GvaTxDbV1 { + tx, + written_block, + written_time, + }| (tx, written_block, written_time), + ) + .collect(), + sending, + pending, + })) + } + // Needed for BMA only, will be removed when the migration is complete. + fn get_tx_by_hash( + dbs_pool: &fast_threadpool::ThreadPoolSyncHandler<SharedDbs<FileBackend>>, + hash: Hash, + profile_path_opt: Option<&Path>, + ) -> KvResult<Option<(TransactionDocumentV10, Option<BlockNumber>)>> { + let gva_db = get_gva_db_ro(profile_path_opt); + dbs_pool + .execute(move |dbs| { + if let Some(tx) = dbs + .txs_mp_db + .txs() + .get(&duniter_core::dbs::HashKeyV2(hash))? + { + Ok(Some((tx.0, None))) + } else if let Some(tx_db) = gva_db.txs().get(&duniter_core::dbs::HashKeyV2(hash))? { + Ok(Some((tx_db.tx, Some(tx_db.written_block.number)))) + } else { + Ok(None) + } + }) + .expect("dbs pool disconnected") + } +} + +impl GvaModule { + async fn start_inner( + conf: GvaConf, + currency: String, + dbs_pool: fast_threadpool::ThreadPoolAsyncHandler<SharedDbs<FileBackend>>, + gva_db_ro: &'static GvaV1DbRo<FileBackend>, + mempools: Mempools, + self_keypair: Ed25519KeyPair, + software_version: &'static str, + ) { + log::info!("GvaServer::start: conf={:?}", conf); + + // Create BcaExecutor and GvaSchema + let self_pubkey = self_keypair.public_key(); + duniter_bca::set_bca_executor( + currency.clone(), + AsyncAccessor::new(), + dbs_pool.clone(), + duniter_gva_dbs_reader::create_dbs_reader(gva_db_ro), + self_keypair, + software_version, + mempools.txs, + ); + let gva_schema = duniter_gva_gql::build_schema_with_data( + duniter_gva_gql::GvaSchemaData { + cm_accessor: AsyncAccessor::new(), + dbs_reader: duniter_gva_dbs_reader::create_dbs_reader(gva_db_ro), + dbs_pool, + server_meta_data: duniter_gva_gql::ServerMetaData { + currency, + self_pubkey, + software_version, + }, + txs_mempool: mempools.txs, + }, + true, + ); + + // Create warp server routes + let graphql_post = warp_::graphql( + &conf, + gva_schema.clone(), + async_graphql::http::MultipartOptions::default(), + ); + + let conf_clone = conf.clone(); + let graphql_playground = + warp::path::path(conf.get_path()) + .and(warp::get()) + .map(move || { + HttpResponse::builder() + .header("content-type", "text/html") + .body(async_graphql::http::playground_source( + GraphQLPlaygroundConfig::new(&format!("/{}", &conf_clone.get_path())) + .subscription_endpoint(&format!( + "/{}", + &conf_clone.get_subscriptions_path(), + )), + )) + }); + + let routes = graphql_playground + .or(graphql_post) + .or(warp_::graphql_ws(&conf, gva_schema.clone())) + .recover(|err: Rejection| async move { + if let Some(warp_::BadRequest(err)) = err.find() { + return Ok::<_, Infallible>(warp::reply::with_status( + err.to_string(), + http::StatusCode::BAD_REQUEST, + )); + } + + Ok(warp::reply::with_status( + "INTERNAL_SERVER_ERROR".to_string(), + http::StatusCode::INTERNAL_SERVER_ERROR, + )) + }); + + // Start warp server + log::info!( + "GVA server listen on http://{}:{}/{}", + conf.get_ip4(), + conf.get_port(), + &conf.get_path() + ); + if let Some(ip6) = conf.get_ip6() { + log::info!( + "GVA server listen on http://{}:{}/{}", + ip6, + conf.get_port(), + &conf.get_path() + ); + futures::future::join( + warp::serve(routes.clone()).run((conf.get_ip4(), conf.get_port())), + warp::serve(routes).run((ip6, conf.get_port())), + ) + .await; + } else { + warp::serve(routes) + .run((conf.get_ip4(), conf.get_port())) + .await; + } + log::warn!("GVA server stopped"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use duniter_core::conf::DuniterConf; + use duniter_core::mempools::Mempools; + use duniter_core::module::DuniterModule; + use fast_threadpool::{ThreadPool, ThreadPoolConfig}; + use unwrap::unwrap; + + #[tokio::test] + #[ignore] + async fn launch_mem_gva() -> anyhow::Result<()> { + let dbs = unwrap!(SharedDbs::mem()); + let threadpool = ThreadPool::start(ThreadPoolConfig::default(), dbs); + + GvaModule::init( + &DuniterConf::default(), + "", + &threadpool.into_async_handler(), + Mempools::default(), + duniter_core::conf::DuniterMode::Start, + None, + "test", + )? + .0 + .start() + .await?; + + Ok(()) + } +} diff --git a/src/warp_.rs b/src/warp_.rs new file mode 100644 index 0000000000000000000000000000000000000000..83a1ba0aa10d3a8eecf4f789b08f1403a1b908de --- /dev/null +++ b/src/warp_.rs @@ -0,0 +1,328 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::{ + net::{IpAddr, SocketAddr}, + time::Duration, +}; + +use bytes::Bytes; + +use crate::anti_spam::{AntiSpam, AntiSpamResponse}; +use crate::*; + +const MAX_BATCH_REQ_PROCESS_DURATION_IN_MILLIS: u64 = 5_000; + +pub struct BadRequest(pub anyhow::Error); + +impl std::fmt::Debug for BadRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl warp::reject::Reject for BadRequest {} + +pub struct ReqExecTooLong; + +impl std::fmt::Debug for ReqExecTooLong { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "server error: request execution too long") + } +} + +impl warp::reject::Reject for ReqExecTooLong {} + +struct GraphQlRequest { + inner: async_graphql::BatchRequest, +} + +impl GraphQlRequest { + fn data<D: std::any::Any + Copy + Send + Sync>(self, data: D) -> Self { + match self.inner { + async_graphql::BatchRequest::Single(request) => { + Self::new(async_graphql::BatchRequest::Single(request.data(data))) + } + async_graphql::BatchRequest::Batch(requests) => { + Self::new(async_graphql::BatchRequest::Batch( + requests.into_iter().map(|req| req.data(data)).collect(), + )) + } + } + } + #[allow(clippy::from_iter_instead_of_collect)] + async fn execute(self, schema: GvaSchema) -> async_graphql::BatchResponse { + use std::iter::FromIterator as _; + match self.inner { + async_graphql::BatchRequest::Single(request) => { + async_graphql::BatchResponse::Single(schema.execute(request).await) + } + async_graphql::BatchRequest::Batch(requests) => async_graphql::BatchResponse::Batch( + futures::stream::FuturesOrdered::from_iter( + requests + .into_iter() + .zip(std::iter::repeat(schema)) + .map(|(request, schema)| async move { schema.execute(request).await }), + ) + .collect() + .await, + ), + } + } + fn len(&self) -> usize { + match &self.inner { + async_graphql::BatchRequest::Single(_) => 1, + async_graphql::BatchRequest::Batch(requests) => requests.len(), + } + } + fn new(inner: async_graphql::BatchRequest) -> Self { + Self { inner } + } + fn single(request: async_graphql::Request) -> Self { + Self::new(async_graphql::BatchRequest::Single(request)) + } +} + +enum ServerResponse { + Bincode(Vec<u8>), + GraphQl(async_graphql::BatchResponse), +} + +impl warp::reply::Reply for ServerResponse { + fn into_response(self) -> warp::reply::Response { + match self { + ServerResponse::Bincode(bytes) => bytes.into_response(), + ServerResponse::GraphQl(gql_batch_resp) => { + let mut resp = warp::reply::with_header( + warp::reply::json(&gql_batch_resp), + "content-type", + "application/json", + ) + .into_response(); + add_cache_control_batch(&mut resp, &gql_batch_resp); + resp + } + } + } +} + +fn add_cache_control_batch( + http_resp: &mut warp::reply::Response, + batch_resp: &async_graphql::BatchResponse, +) { + match batch_resp { + async_graphql::BatchResponse::Single(resp) => add_cache_control(http_resp, resp), + async_graphql::BatchResponse::Batch(resps) => { + for resp in resps { + add_cache_control(http_resp, resp) + } + } + } +} + +fn add_cache_control(http_resp: &mut warp::reply::Response, resp: &async_graphql::Response) { + if resp.is_ok() { + if let Some(cache_control) = resp.cache_control.value() { + if let Ok(value) = cache_control.parse() { + http_resp.headers_mut().insert("cache-control", value); + } + } + } +} + +pub(crate) fn graphql( + conf: &GvaConf, + gva_schema: GvaSchema, + opts: async_graphql::http::MultipartOptions, +) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone { + let anti_spam = AntiSpam::from(conf); + let opts = Arc::new(opts); + warp::path::path(conf.get_path()) + .and(warp::method()) + .and(warp::query::raw().or(warp::any().map(String::new)).unify()) + .and(warp::addr::remote()) + .and(warp::header::optional::<IpAddr>("X-Real-IP")) + .and(warp::header::optional::<String>("content-type")) + .and(warp::body::stream()) + .and(warp::any().map(move || anti_spam.clone())) + .and(warp::any().map(move || gva_schema.clone())) + .and(warp::any().map(move || opts.clone())) + .and_then( + |method, + query: String, + remote_addr: Option<SocketAddr>, + x_real_ip: Option<IpAddr>, + content_type: Option<String>, + body, + anti_spam: AntiSpam, + gva_schema: GvaSchema, + opts: Arc<async_graphql::http::MultipartOptions>| async move { + let AntiSpamResponse { + is_whitelisted, + is_ok, + } = anti_spam + .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip()))) + .await; + if is_ok { + if method == http::Method::GET { + let request: async_graphql::Request = serde_urlencoded::from_str(&query) + .map_err(|err| warp::reject::custom(BadRequest(err.into())))?; + Ok(ServerResponse::GraphQl( + GraphQlRequest::single(request.data(QueryContext { is_whitelisted })) + .execute(gva_schema) + .await, + )) + } else { + let body_stream = futures::TryStreamExt::map_err(body, |err| { + std::io::Error::new(std::io::ErrorKind::Other, err) + }) + .map_ok(|mut buf| { + let remaining = warp::Buf::remaining(&buf); + warp::Buf::copy_to_bytes(&mut buf, remaining) + }); + if content_type.as_deref() == Some("application/bincode") { + tokio::time::timeout( + Duration::from_millis(MAX_BATCH_REQ_PROCESS_DURATION_IN_MILLIS), + process_bincode_batch_queries(body_stream, is_whitelisted), + ) + .await + .map_err(|_| warp::reject::custom(ReqExecTooLong))? + } else { + tokio::time::timeout( + Duration::from_millis(MAX_BATCH_REQ_PROCESS_DURATION_IN_MILLIS), + process_json_batch_queries( + body_stream.into_async_read(), + content_type, + gva_schema, + is_whitelisted, + *opts, + ), + ) + .await + .map_err(|_| warp::reject::custom(ReqExecTooLong))? + } + } + } else { + Err(warp::reject::custom(BadRequest(anyhow::Error::msg( + r#"{ "error": "too many requests" }"#, + )))) + } + }, + ) +} + +async fn process_bincode_batch_queries( + body_reader: impl 'static + futures::TryStream<Ok = Bytes, Error = std::io::Error> + Send + Unpin, + is_whitelisted: bool, +) -> Result<ServerResponse, warp::Rejection> { + Ok(ServerResponse::Bincode( + duniter_bca::execute(body_reader, is_whitelisted).await, + )) +} + +async fn process_json_batch_queries( + body_reader: impl 'static + futures::AsyncRead + Send + Unpin, + content_type: Option<String>, + gva_schema: GvaSchema, + is_whitelisted: bool, + opts: async_graphql::http::MultipartOptions, +) -> Result<ServerResponse, warp::Rejection> { + let batch_request = GraphQlRequest::new( + async_graphql::http::receive_batch_body( + content_type, + body_reader, + async_graphql::http::MultipartOptions::clone(&opts), + ) + .await + .map_err(|err| warp::reject::custom(BadRequest(err.into())))?, + ); + if is_whitelisted || batch_request.len() <= anti_spam::MAX_BATCH_SIZE { + Ok(ServerResponse::GraphQl( + batch_request + .data(QueryContext { is_whitelisted }) + .execute(gva_schema) + .await, + )) + } else { + Err(warp::reject::custom(BadRequest(anyhow::Error::msg( + r#"{ "error": "The batch contains too many requests" }"#, + )))) + } +} + +pub(crate) fn graphql_ws( + conf: &GvaConf, + schema: GvaSchema, +) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = Rejection> + Clone { + let anti_spam = AntiSpam::from(conf); + warp::path::path(conf.get_subscriptions_path()) + .and(warp::addr::remote()) + .and(warp::header::optional::<IpAddr>("X-Real-IP")) + .and(warp::ws()) + .and(warp::any().map(move || schema.clone())) + .and(warp::any().map(move || anti_spam.clone())) + .and_then( + |remote_addr: Option<SocketAddr>, + x_real_ip: Option<IpAddr>, + ws: warp::ws::Ws, + schema: GvaSchema, + anti_spam: AntiSpam| async move { + let AntiSpamResponse { + is_whitelisted: _, + is_ok, + } = anti_spam + .verify(x_real_ip.or_else(|| remote_addr.map(|ra| ra.ip()))) + .await; + if is_ok { + Ok((ws, schema)) + } else { + Err(warp::reject::custom(BadRequest(anyhow::Error::msg( + r#"{ "error": "too many requests" }"#, + )))) + } + }, + ) + .and_then(|(ws, schema): (warp::ws::Ws, GvaSchema)| { + let reply = ws.on_upgrade(move |websocket| { + let (ws_sender, ws_receiver) = websocket.split(); + + async move { + let _ = async_graphql::http::WebSocket::new( + schema, + ws_receiver + .take_while(|msg| futures::future::ready(msg.is_ok())) + .map(Result::unwrap) + .map(warp::ws::Message::into_bytes), + async_graphql::http::WebSocketProtocols::SubscriptionsTransportWS, + ) + .map(|ws_msg| match ws_msg { + async_graphql::http::WsMessage::Text(s) => warp::ws::Message::text(s), + async_graphql::http::WsMessage::Close(code, reason) => { + warp::ws::Message::close_with(code, reason) + } + }) + .map(Ok) + .forward(ws_sender) + .await; + } + }); + + futures::future::ready(Ok::<_, Rejection>(warp::reply::with_header( + reply, + "Sec-WebSocket-Protocol", + "graphql-ws", + ))) + }) +}