diff --git a/CODEOWNERS b/CODEOWNERS index ede34e3fe78..ed34b9d75da 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,7 +3,7 @@ /chain/ @SkidanovAlex @bowenwang1996 /chain/jsonrpc/ @frol @evgenykuzyakov /chain/indexer/ @khorolets @frol -/chain/rosettarpc/ @frol +/chain/rosetta-rpc/ @frol /runtime/ @evgenykuzyakov @chain-police /runtime/runtime-params-estimator/ @olonho @willemneal diff --git a/Cargo.lock b/Cargo.lock index 3052c7d3264..27c8d20ff78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot", + "parking_lot 0.10.2", "pin-project", "smallvec", "tokio", @@ -109,7 +109,7 @@ dependencies = [ "lazy_static", "log", "mime", - "percent-encoding", + "percent-encoding 2.1.0", "pin-project", "rand 0.7.3", "regex", @@ -214,7 +214,7 @@ dependencies = [ "lazy_static", "log", "num_cpus", - "parking_lot", + "parking_lot 0.10.2", "threadpool", ] @@ -288,7 +288,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "time", - "url", + "url 2.1.1", ] [[package]] @@ -418,7 +418,7 @@ dependencies = [ "log", "mime", "openssl", - "percent-encoding", + "percent-encoding 2.1.0", "rand 0.7.3", "serde", "serde_json", @@ -803,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb51c9e75b94452505acd21d929323f5a5c6c4735a852adbd39ef5fb1b014f30" dependencies = [ "heck", - "proc-macro-error", + "proc-macro-error 0.4.12", "proc-macro2", "quote", "syn", @@ -839,6 +839,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "cloudabi" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" +dependencies = [ + "bitflags", +] + [[package]] name = "cmake" version = "0.1.42" @@ -1845,6 +1854,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.2.0" @@ -1911,6 +1931,12 @@ dependencies = [ "regex", ] +[[package]] +name = "instant" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b141fdc7836c525d4d594027d318c84161ca17aaf8113ab1f81ab93ae897485" + [[package]] name = "iovec" version = "0.1.4" @@ -2143,6 +2169,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lock_api" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" +dependencies = [ + "scopeguard", + "serde", +] + [[package]] name = "log" version = "0.4.8" @@ -2648,6 +2684,29 @@ dependencies = [ "validator_derive", ] +[[package]] +name = "near-rosetta-rpc" +version = "0.1.0" +dependencies = [ + "actix", + "actix-cors", + "actix-web", + "futures", + "lazy_static", + "near-chain-configs", + "near-client", + "near-crypto", + "near-network", + "near-primitives", + "near-runtime-configs", + "paperclip", + "serde", + "serde_json", + "strum", + "tokio", + "validator", +] + [[package]] name = "near-rpc-error-core" version = "0.1.0" @@ -2849,6 +2908,7 @@ dependencies = [ "near-network", "near-pool", "near-primitives", + "near-rosetta-rpc", "near-runtime-configs", "near-store", "near-telemetry", @@ -3103,6 +3163,79 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "paperclip" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0bd32ec54c2ee650047a202933eed8659f60fbcfc760b11b6e057f661508c65" +dependencies = [ + "anyhow", + "itertools 0.9.0", + "paperclip-actix", + "paperclip-core", + "paperclip-macros", + "parking_lot 0.11.0", + "semver 0.10.0", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "thiserror", + "url 1.7.2", +] + +[[package]] +name = "paperclip-actix" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b36b02b2d98d31894f84e987bacc827d8e24c708d6881b2a83fc1c98a0b9d312" +dependencies = [ + "actix-service", + "actix-web", + "futures", + "lazy_static", + "paperclip-core", + "paperclip-macros", + "parking_lot 0.11.0", + "serde_json", +] + +[[package]] +name = "paperclip-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d6425b0fa9f9ca3a0558e1d0739c76609d03e4a9e073b6f98fd31c8c2aa479" +dependencies = [ + "actix-http", + "actix-web", + "lazy_static", + "mime", + "paperclip-macros", + "parking_lot 0.11.0", + "regex", + "serde", + "serde_json", + "serde_yaml", + "thiserror", +] + +[[package]] +name = "paperclip-macros" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fab122a30c2ea222c227f8817782619c295846c512ad79b07af2b36897dec4b0" +dependencies = [ + "heck", + "http", + "lazy_static", + "proc-macro-error 1.0.4", + "proc-macro2", + "quote", + "strum", + "strum_macros", + "syn", +] + [[package]] name = "parity-secp256k1" version = "0.7.0" @@ -3127,8 +3260,19 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" dependencies = [ - "lock_api", - "parking_lot_core", + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" +dependencies = [ + "instant", + "lock_api 0.4.1", + "parking_lot_core 0.8.0", ] [[package]] @@ -3138,7 +3282,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" dependencies = [ "cfg-if", - "cloudabi", + "cloudabi 0.0.3", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" +dependencies = [ + "cfg-if", + "cloudabi 0.1.0", + "instant", "libc", "redox_syscall", "smallvec", @@ -3151,6 +3310,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3223,7 +3388,20 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" dependencies = [ - "proc-macro-error-attr", + "proc-macro-error-attr 0.4.12", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr 1.0.4", "proc-macro2", "quote", "syn", @@ -3243,6 +3421,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.15" @@ -3257,9 +3446,9 @@ checksum = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" [[package]] name = "proc-macro2" -version = "1.0.10" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" dependencies = [ "unicode-xid", ] @@ -3594,7 +3783,7 @@ dependencies = [ "mime", "mime_guess", "native-tls", - "percent-encoding", + "percent-encoding 2.1.0", "pin-project-lite", "rustls", "serde", @@ -3603,7 +3792,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-tls", - "url", + "url 2.1.1", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3722,7 +3911,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", ] [[package]] @@ -3822,6 +4011,15 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver-parser" version = "0.7.0" @@ -3830,9 +4028,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.106" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" dependencies = [ "serde_derive", ] @@ -3858,9 +4056,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.106" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" dependencies = [ "proc-macro2", "quote", @@ -3888,7 +4086,19 @@ dependencies = [ "dtoa", "itoa", "serde", - "url", + "url 2.1.1", +] + +[[package]] +name = "serde_yaml" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3e2dd40a7cdc18ca80db804b7f461a39bb721160a85c9a1fa30134bf3c02a5" +dependencies = [ + "dtoa", + "linked-hash-map", + "serde", + "yaml-rust", ] [[package]] @@ -4108,9 +4318,9 @@ checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" [[package]] name = "syn" -version = "1.0.18" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410a7488c0a728c7ceb4ad59b9567eb4053d02e8cc7f5c0e0eeeb39518369213" +checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" dependencies = [ "proc-macro2", "quote", @@ -4235,18 +4445,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.15" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b3d3d2ff68104100ab257bb6bb0cb26c901abe4bd4ba15961f3bf867924012" +checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.15" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca972988113b7715266f91250ddb98070d033c62a011fa0fcc57434a649310dd" +checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" dependencies = [ "proc-macro2", "quote", @@ -4478,14 +4688,14 @@ dependencies = [ "enum-as-inner", "failure", "futures", - "idna", + "idna 0.2.0", "lazy_static", "log", "rand 0.7.3", "smallvec", "socket2", "tokio", - "url", + "url 2.1.1", ] [[package]] @@ -4600,15 +4810,26 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60369ef7a31de49bcb3f6ca728d4ba7300d9a1658f94c727d4cab8c8d9f4aece" +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +dependencies = [ + "idna 0.1.5", + "matches", + "percent-encoding 1.0.1", +] + [[package]] name = "url" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" dependencies = [ - "idna", + "idna 0.2.0", "matches", - "percent-encoding", + "percent-encoding 2.1.0", ] [[package]] @@ -4626,13 +4847,13 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab5990ba09102e1ddc954d294f09b9ea00fc7831a5813bbe84bfdbcae44051e" dependencies = [ - "idna", + "idna 0.2.0", "lazy_static", "regex", "serde", "serde_derive", "serde_json", - "url", + "url 2.1.1", ] [[package]] @@ -4838,7 +5059,7 @@ dependencies = [ "libc", "nix", "page_size", - "parking_lot", + "parking_lot 0.10.2", "rustc_version", "serde", "serde-bench", @@ -5134,6 +5355,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index cf6b90e2ca8..a0602994f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "chain/jsonrpc", "chain/jsonrpc/client", "chain/jsonrpc/test-utils", + "chain/rosetta-rpc", "test-utils/testlib", "test-utils/loadtester", "test-utils/state-viewer", @@ -104,3 +105,4 @@ adversarial = ["neard/adversarial", "near-jsonrpc/adversarial", "near-store/adve no_cache = ["neard/no_cache"] metric_recorder = ["neard/metric_recorder"] delay_detector = ["neard/delay_detector"] +rosetta_rpc = ["neard/rosetta_rpc"] diff --git a/chain/rosetta-rpc/CHANGELOG.md b/chain/rosetta-rpc/CHANGELOG.md new file mode 100644 index 00000000000..52de45e1f66 --- /dev/null +++ b/chain/rosetta-rpc/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 0.1.0 + +* Data API exposes feature-complete balance-changing operations +* Construction API exposes the bare minimum signed transaction submittion API diff --git a/chain/rosetta-rpc/Cargo.toml b/chain/rosetta-rpc/Cargo.toml new file mode 100644 index 00000000000..b8e8bff7c61 --- /dev/null +++ b/chain/rosetta-rpc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "near-rosetta-rpc" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4" +actix = "0.9" +actix-web = "2" +actix-cors = "0.2" +futures = "0.3.5" +tokio = { version = "0.2", features = ["full"] } +paperclip = { version = "0.4", features = ["actix", "actix-nightly"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +strum = { version = "0.18", features = ["derive"] } +validator = "0.10" + +near-primitives = { path = "../../core/primitives" } +near-crypto = { path = "../../core/crypto" } +near-chain-configs = { path = "../../core/chain-configs" } +near-client = { path = "../client" } +near-network = { path = "../network" } +near-runtime-configs = { path = "../../core/runtime-configs" } diff --git a/chain/rosetta-rpc/README.md b/chain/rosetta-rpc/README.md new file mode 100644 index 00000000000..037575677b0 --- /dev/null +++ b/chain/rosetta-rpc/README.md @@ -0,0 +1,114 @@ +Rosetta API Extension for nearcore +================================== + +Rosetta is a public API spec defined to be a common denominator for blockchain projects. + +Rosetta RPC is built into nearcore and it happily co-exist with JSON RPC. + +* [Rosetta Homepage](https://www.rosetta-api.org/docs/welcome.html) +* [Rosetta API Specification](https://github.com/coinbase/rosetta-specifications) +* [Rosetta Tooling](https://github.com/coinbase/rosetta-cli) + +You can view Rosetta API specification in [OpenAPI (Swagger) UI](https://petstore.swagger.io/) +passing the link to Rosetta OpenAPI specification: + +``` +https://raw.githubusercontent.com/coinbase/rosetta-specifications/master/api.json +``` + +Also, Rosetta implementation in nearcore exposes auto-generated OpenAPI +specification that has some extra comments regarding to the particular +implementation, and you can always access it from the running node: + +``` +http://localhost:3040/api/spec +``` + +Supported Features +------------------ + +Our current goal is to have a minimal yet feature-complete implementation of +Rosetta RPC serving +[the main use-case Rosetta was designed for](https://community.rosetta-api.org/t/what-is-rosetta-main-use-case/92/2), +that is exposing balance-changing operations in a consistent way enabling +reconciliation through tracking individual blocks and transactions. + +The Rosetta APIs are organized into two distinct categories, the Data API and +the Construction API. Simply put, the Data API is for retrieving data from a +blockchain network and the Construction API is for constructing and submitting +transactions to a blockchain network. + +| Feature | Status | +| ----------------------------- | ------------------------------------------------------------- | +| Data API | Feature-complete with some quirks | +| - `/network/list` | Done | +| - `/network/status` | Done | +| - `/network/options` | Done | +| - `/block` | Feature-complete (exposes only balance-changing operations) | +| - `/block/transaction` | Feature-complete (exposes only balance-changing operations and the implementation is suboptimal from the performance point of view) | +| - `/account/balance` | Done (properly exposes liquid, liquid for storage, and locked [staked] balances through sub-accounts) | +| - `/mempool` | Not implemented as mempool does not hold transactions for any meaningful time | +| - `/mempool/transaction` | Not implemented (see above) | +| Construction API | Partially implemented | +| - `/construction/derive` | Done (not applicable to NEAR) | +| - `/construction/preprocess` | Not implemented | +| - `/construction/metadata` | Not implemented | +| - `/construction/payloads` | Not implemented | +| - `/construction/combine` | Not implemented | +| - `/construction/parse` | Not implemented | +| - `/construction/hash` | Done | +| - `/construction/submit` | Done | + +To verify the API compliance use: + +``` +rosetta-cli check:data --configuration-file=./rosetta.cfg +rosetta-cli check:construction --configuration-file=./rosetta.cfg +``` + +How to Run +---------- + +Follow [the standard nearcore procedures to run a node compiled from the source code](https://docs.near.org/docs/contribution/nearcore) +enabling `rosettarpc` feature: + +``` +cargo run --release --package neard --bin neard --features rosetta_rpc -- init +cargo run --release --package neard --bin neard --features rosetta_rpc -- run +``` + +By default, Rosetta RPC is available on port TCP/3040. + + +Tweaks +------ + +By default, nearcore is configured to do as little work as possible while still +operating on an up-to-date state. Indexers may have different requirements, so +there is no solution that would work for everyone, and thus we are going to +provide you with the set of knobs you can tune for your requirements. + +As already has been mentioned in this README, the most common tweak you need to +apply is listing all the shards you want to index data from; to do that, you +should ensure that `"tracked_shards"` in the `~/.near//config.json` +lists all the shard IDs, e.g. for the current betanet and testnet, which have a +single shard: + +``` +... +"tracked_shards": [0], +... +``` + +By default, nearcore is configured to automatically clean old data (performs +garbage collection [GC]), so querying the data that was observed a few epochs +before may return an error saying that the data is missing. If you only need +recent blocks, you don't need this tweak, but if you need access to the +historical data, consider updating `"archive"` setting in `config.json` to +`true`: + +``` +... +"archive": true, +... +``` diff --git a/chain/rosetta-rpc/rosetta.cfg b/chain/rosetta-rpc/rosetta.cfg new file mode 100644 index 00000000000..59fb01c0de6 --- /dev/null +++ b/chain/rosetta-rpc/rosetta.cfg @@ -0,0 +1,92 @@ +{ + "network": { + "blockchain": "nearprotocol", + "network": "testnet" + }, + "online_url": "http://localhost:3040", + "data_directory": "", + "http_timeout": 10, + "retry_elapsed_time": 60, + "sync_concurrency": 8, + "transaction_concurrency": 16, + "tip_delay": 300, + "disable_memory_limit": false, + "log_configuration": false, + "construction": { + "offline_url": "http://localhost:3040", + "currency": { + "symbol": "yoctoNEAR", + "decimals": 0 + }, + "minimum_balance": "0", + "maximum_fee": "5000000000000000", + "curve_type": "secp256k1", + "accounting_model": "account", + "scenario": [ + { + "operation_identifier": { + "index": 0 + }, + "type": "transfer", + "status": "", + "account": { + "address": "{{ SENDER }}" + }, + "amount": { + "value": "{{ SENDER_VALUE }}", + "currency": null + } + }, + { + "operation_identifier": { + "index": 1 + }, + "related_operations": [ + { + "index": 0 + } + ], + "type": "transfer", + "status": "", + "account": { + "address": "{{ RECIPIENT }}" + }, + "amount": { + "value": "{{ RECIPIENT_VALUE }}", + "currency": null + } + } + ], + "confirmation_depth": 10, + "stale_depth": 30, + "broadcast_limit": 3, + "ignore_broadcast_failures": false, + "change_scenario": null, + "clear_broadcasts": false, + "broadcast_behind_tip": false, + "block_broadcast_limit": 5, + "rebroadcast_all": false, + "new_account_probability": 0.5, + "max_addresses": 200 + }, + "data": { + "start_index": 12140933, + "active_reconciliation_concurrency": 16, + "inactive_reconciliation_concurrency": 4, + "inactive_reconciliation_frequency": 250, + "log_blocks": false, + "log_transactions": false, + "log_balance_changes": false, + "log_reconciliations": false, + "ignore_reconciliation_error": false, + "exempt_accounts": "", + "bootstrap_balances": "", + "historical_balance_disabled": false, + "interesting_accounts": "", + "reconciliation_disabled": false, + "inactive_discrepency_search_disabled": false, + "balance_tracking_disabled": false, + "coin_tracking_disabled": false, + "results_output_file": "" + } +} diff --git a/chain/rosetta-rpc/src/adapters.rs b/chain/rosetta-rpc/src/adapters.rs new file mode 100644 index 00000000000..8e27882ccd0 --- /dev/null +++ b/chain/rosetta-rpc/src/adapters.rs @@ -0,0 +1,383 @@ +use std::sync::Arc; + +use actix::Addr; + +use near_chain_configs::Genesis; +use near_client::ViewClientActor; +use near_primitives::serialize::BaseEncode; + +/// NEAR Protocol defines initial state in genesis records and treats the first +/// block differently (e.g. it cannot contain any transactions: https://stackoverflow.com/a/63347167/1178806). +/// +/// Genesis records can be huge (order of gigabytes of JSON data), and Rosetta +/// API does not define any pagination, and suggests to use +/// `other_transactions` to deal with this: https://community.rosetta-api.org/t/how-to-return-data-without-being-able-to-paginate/98 +/// We choose to do a proper implementation for the genesis block later. +async fn convert_genesis_records_to_transaction( + genesis: Arc, + view_client_addr: Addr, + block: &near_primitives::views::BlockView, +) -> Result { + let genesis_account_ids = genesis.records.as_ref().iter().filter_map(|record| { + if let near_primitives::state_record::StateRecord::Account { account_id, .. } = record { + Some(account_id) + } else { + None + } + }); + let genesis_accounts = crate::utils::query_accounts( + genesis_account_ids, + &near_primitives::types::BlockId::Hash(block.header.hash).into(), + &view_client_addr, + ) + .await?; + + let mut operations = Vec::new(); + for (account_id, account) in genesis_accounts { + let account_balances = crate::utils::RosettaAccountBalances::from_account( + &account, + &genesis.config.runtime_config, + ); + + if account_balances.liquid != 0 { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: None, + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear(account_balances.liquid)), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if account_balances.liquid_for_storage != 0 { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some(crate::models::SubAccount::LiquidBalanceForStorage.into()), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear( + account_balances.liquid_for_storage, + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if account_balances.locked != 0 { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some(crate::models::SubAccount::Locked.into()), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear(account_balances.locked)), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + } + + Ok(crate::models::Transaction { + transaction_identifier: crate::models::TransactionIdentifier { + hash: format!("block:{}", block.header.hash), + }, + operations, + metadata: crate::models::TransactionMetadata { + type_: crate::models::TransactionType::Block, + }, + }) +} + +pub(crate) async fn convert_block_to_transactions( + genesis: Arc, + view_client_addr: Addr, + block: &near_primitives::views::BlockView, +) -> Result, crate::errors::ErrorKind> { + let runtime_config = &genesis.config.runtime_config; + + let state_changes = view_client_addr + .send(near_client::GetStateChangesInBlock { block_hash: block.header.hash }) + .await? + .unwrap(); + + let touched_account_ids = state_changes + .into_iter() + .filter_map(|x| { + if let near_primitives::views::StateChangeKindView::AccountTouched { account_id } = x { + Some(account_id) + } else { + None + } + }) + .collect::>(); + + let prev_block_id = near_primitives::types::BlockReference::from( + near_primitives::types::BlockId::Hash(block.header.prev_hash), + ); + let mut accounts_previous_state = + crate::utils::query_accounts(touched_account_ids.iter(), &prev_block_id, &view_client_addr) + .await?; + + let accounts_changes = view_client_addr + .send(near_client::GetStateChanges { + block_hash: block.header.hash, + state_changes_request: + near_primitives::views::StateChangesRequestView::AccountChanges { + account_ids: touched_account_ids.into_iter().collect(), + }, + }) + .await? + .map_err(crate::errors::ErrorKind::InternalError)?; + + let mut transactions = Vec::::new(); + for account_change in accounts_changes.into_iter() { + let transaction_hash = match account_change.cause { + near_primitives::views::StateChangeCauseView::TransactionProcessing { tx_hash } => { + format!("tx:{}", tx_hash.to_base()) + } + near_primitives::views::StateChangeCauseView::ActionReceiptProcessingStarted { + receipt_hash, + } => format!("receipt:{}", receipt_hash.to_base()), + near_primitives::views::StateChangeCauseView::ActionReceiptGasReward { + receipt_hash, + } => format!("receipt:{}", receipt_hash.to_base()), + near_primitives::views::StateChangeCauseView::ReceiptProcessing { receipt_hash } => { + format!("receipt:{}", receipt_hash.to_base()) + } + near_primitives::views::StateChangeCauseView::PostponedReceipt { receipt_hash } => { + format!("receipt:{}", receipt_hash.to_base()) + } + near_primitives::views::StateChangeCauseView::InitialState + | near_primitives::views::StateChangeCauseView::ValidatorAccountsUpdate + | near_primitives::views::StateChangeCauseView::UpdatedDelayedReceipts => { + format!("block:{}", block.header.hash) + } + near_primitives::views::StateChangeCauseView::NotWritableToDisk => unreachable!(), + }; + let current_transaction = if let Some(transaction) = transactions.last_mut() { + if transaction.transaction_identifier.hash == transaction_hash { + Some(transaction) + } else { + None + } + } else { + None + }; + let current_transaction = if let Some(transaction) = current_transaction { + transaction + } else { + transactions.push(crate::models::Transaction { + transaction_identifier: crate::models::TransactionIdentifier { + hash: transaction_hash.clone(), + }, + operations: vec![], + metadata: crate::models::TransactionMetadata { + type_: crate::models::TransactionType::Transaction, + }, + }); + transactions.last_mut().unwrap() + }; + let operations = &mut current_transaction.operations; + match account_change.value { + near_primitives::views::StateChangeValueView::AccountUpdate { account_id, account } => { + let previous_account_state = accounts_previous_state.get(&account_id); + + let previous_account_balances = previous_account_state + .map(|account| { + crate::utils::RosettaAccountBalances::from_account(account, runtime_config) + }) + .unwrap_or_else(crate::utils::RosettaAccountBalances::zero); + + let new_account_balances = + crate::utils::RosettaAccountBalances::from_account(&account, runtime_config); + + if previous_account_balances.liquid != new_account_balances.liquid { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: None, + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.liquid, + new_account_balances.liquid, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if previous_account_balances.liquid_for_storage + != new_account_balances.liquid_for_storage + { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some( + crate::models::SubAccount::LiquidBalanceForStorage.into(), + ), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.liquid_for_storage, + new_account_balances.liquid_for_storage, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if previous_account_balances.locked != new_account_balances.locked { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some(crate::models::SubAccount::Locked.into()), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.locked, + new_account_balances.locked, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + accounts_previous_state.insert(account_id, account); + } + + near_primitives::views::StateChangeValueView::AccountDeletion { account_id } => { + let previous_account_state = accounts_previous_state.get(&account_id); + + let previous_account_balances = + if let Some(previous_account_state) = previous_account_state { + crate::utils::RosettaAccountBalances::from_account( + previous_account_state, + runtime_config, + ) + } else { + continue; + }; + let new_account_balances = crate::utils::RosettaAccountBalances::zero(); + + if previous_account_balances.liquid != new_account_balances.liquid { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: None, + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.liquid, + new_account_balances.liquid, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if previous_account_balances.liquid_for_storage + != new_account_balances.liquid_for_storage + { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some( + crate::models::SubAccount::LiquidBalanceForStorage.into(), + ), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.liquid_for_storage, + new_account_balances.liquid_for_storage, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + if previous_account_balances.locked != new_account_balances.locked { + operations.push(crate::models::Operation { + operation_identifier: crate::models::OperationIdentifier::new(&operations), + related_operations: None, + account: Some(crate::models::AccountIdentifier { + address: account_id.clone(), + sub_account: Some(crate::models::SubAccount::Locked.into()), + metadata: None, + }), + amount: Some(crate::models::Amount::from_yoctonear_diff( + crate::utils::SignedDiff::cmp( + previous_account_balances.locked, + new_account_balances.locked, + ), + )), + type_: crate::models::OperationType::Transfer, + status: crate::models::OperationStatusKind::Success, + metadata: None, + }); + } + + accounts_previous_state.remove(&account_id); + } + unexpected_value => { + return Err(crate::errors::ErrorKind::InternalInvariantError(format!( + "queried AccountChanges, but received {:?}.", + unexpected_value + )) + .into()) + } + } + } + + Ok(transactions) +} + +pub(crate) async fn collect_transactions( + genesis: Arc, + view_client_addr: Addr, + block: &near_primitives::views::BlockView, +) -> Result, crate::errors::ErrorKind> { + if block.header.prev_hash == Default::default() { + Ok(vec![convert_genesis_records_to_transaction(genesis, view_client_addr, block).await?]) + } else { + convert_block_to_transactions(genesis, view_client_addr, block).await + } +} diff --git a/chain/rosetta-rpc/src/config.rs b/chain/rosetta-rpc/src/config.rs new file mode 100644 index 00000000000..1330a4fe9c1 --- /dev/null +++ b/chain/rosetta-rpc/src/config.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RosettaRpcConfig { + pub addr: String, + pub cors_allowed_origins: Vec, +} + +impl Default for RosettaRpcConfig { + fn default() -> Self { + Self { addr: "0.0.0.0:3040".to_owned(), cors_allowed_origins: vec!["*".to_owned()] } + } +} + +impl RosettaRpcConfig { + pub fn new(addr: &str) -> Self { + Self { addr: addr.to_owned(), ..Default::default() } + } +} diff --git a/chain/rosetta-rpc/src/consts.rs b/chain/rosetta-rpc/src/consts.rs new file mode 100644 index 00000000000..3c336c1c17a --- /dev/null +++ b/chain/rosetta-rpc/src/consts.rs @@ -0,0 +1,6 @@ +use crate::models; + +lazy_static::lazy_static! { + pub(crate) static ref YOCTO_NEAR_CURRENCY: models::Currency = + models::Currency { symbol: "yoctoNEAR".to_string(), decimals: 0, metadata: None }; +} diff --git a/chain/rosetta-rpc/src/errors.rs b/chain/rosetta-rpc/src/errors.rs new file mode 100644 index 00000000000..7b097a5d5ca --- /dev/null +++ b/chain/rosetta-rpc/src/errors.rs @@ -0,0 +1,52 @@ +#[derive(Debug, strum::EnumIter)] +pub(crate) enum ErrorKind { + InvalidInput(String), + NotFound(String), + WrongNetwork(String), + Timeout(String), + InternalInvariantError(String), + InternalError(String), +} + +impl std::convert::From for ErrorKind { + fn from(err: actix::MailboxError) -> Self { + Self::InternalError(format!( + "Server seems to be under a heavy load thus reaching a limit of Actix queue: {}", + err + )) + } +} + +impl std::convert::From for ErrorKind { + fn from(_: tokio::time::Elapsed) -> Self { + Self::Timeout("The operation timed out.".to_string()) + } +} + +impl std::convert::From for ErrorKind { + fn from(err: near_client::TxStatusError) -> Self { + match err { + near_client::TxStatusError::ChainError(err) => Self::InternalInvariantError(format!( + "Transaction could not be found due to an internal error: {:?}", + err + )), + near_client::TxStatusError::MissingTransaction(err) => { + Self::NotFound(format!("Transaction is missing: {:?}", err)) + } + near_client::TxStatusError::InvalidTx(err) => Self::NotFound(format!( + "Transaction is invalid, so it will never be included to the chain: {:?}", + err + )), + near_client::TxStatusError::InternalError + | near_client::TxStatusError::TimeoutError => { + // TODO: remove the statuses from TxStatusError since they are + // never constructed by the view client (it is a leak of + // abstraction introduced in JSONRPC) + Self::InternalInvariantError(format!( + "TxStatusError reached unexpected state: {:?}", + err + )) + } + } + } +} diff --git a/chain/rosetta-rpc/src/lib.rs b/chain/rosetta-rpc/src/lib.rs new file mode 100644 index 00000000000..12fbf51a20a --- /dev/null +++ b/chain/rosetta-rpc/src/lib.rs @@ -0,0 +1,757 @@ +use std::convert::TryInto; +use std::sync::Arc; + +use actix::Addr; +use actix_cors::{Cors, CorsFactory}; +use actix_web::{App, HttpServer}; +use paperclip::actix::{ + api_v2_operation, + web::{self, Json}, + OpenApiExt, +}; +use strum::IntoEnumIterator; + +use near_chain_configs::Genesis; +use near_client::{ClientActor, ViewClientActor}; +use near_primitives::serialize::BaseEncode; + +pub use config::RosettaRpcConfig; + +mod adapters; +mod config; +mod consts; +mod errors; +mod models; +mod utils; + +pub const BASE_PATH: &str = ""; +pub const API_VERSION: &str = "1.4.2"; + +/// Get List of Available Networks +/// +/// This endpoint returns a list of NetworkIdentifiers that the Rosetta server +/// supports. +#[api_v2_operation] +async fn network_list( + client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + Ok(Json(models::NetworkListResponse { + network_identifiers: vec![models::NetworkIdentifier { + blockchain: "nearprotocol".to_owned(), + network: status.chain_id, + sub_network_identifier: None, + }], + })) +} + +#[api_v2_operation] +/// Get Network Status +/// +/// This endpoint returns the current status of the network requested. Any +/// NetworkIdentifier returned by /network/list should be accessible here. +async fn network_status( + genesis: web::Data>, + client_addr: web::Data>, + view_client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != body.network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + let genesis_height = genesis.config.genesis_height; + let (network_info, genesis_block, earliest_block) = tokio::try_join!( + client_addr.send(near_client::GetNetworkInfo {}), + view_client_addr.send(near_client::GetBlock( + near_primitives::types::BlockId::Height(genesis_height).into(), + )), + view_client_addr.send(near_client::GetBlock( + near_primitives::types::BlockReference::SyncCheckpoint( + near_primitives::types::SyncCheckpoint::EarliestAvailable + ), + )), + )?; + let network_info = network_info.map_err(errors::ErrorKind::InternalError)?; + let genesis_block = genesis_block.map_err(errors::ErrorKind::InternalInvariantError)?; + let earliest_block = earliest_block; + + let genesis_block_identifier: models::BlockIdentifier = (&genesis_block.header).into(); + let oldest_block_identifier: models::BlockIdentifier = earliest_block + .ok() + .map(|block| (&block.header).into()) + .unwrap_or_else(|| genesis_block_identifier.clone()); + Ok(Json(models::NetworkStatusResponse { + current_block_identifier: models::BlockIdentifier { + index: status.sync_info.latest_block_height.try_into().unwrap(), + hash: status.sync_info.latest_block_hash.to_base(), + }, + current_block_timestamp: status.sync_info.latest_block_time.timestamp_millis(), + genesis_block_identifier, + oldest_block_identifier, + sync_status: if status.sync_info.syncing { + Some(models::SyncStatus { + current_index: status.sync_info.latest_block_height.try_into().unwrap(), + target_index: None, + stage: None, + }) + } else { + None + }, + peers: network_info + .active_peers + .into_iter() + .map(|peer| models::Peer { peer_id: peer.id.to_string(), metadata: None }) + .collect(), + })) +} + +#[api_v2_operation] +/// Get Network Options +/// +/// This endpoint returns the version information and allowed network-specific +/// types for a NetworkIdentifier. Any NetworkIdentifier returned by +/// /network/list should be accessible here. Because options are retrievable in +/// the context of a NetworkIdentifier, it is possible to define unique options +/// for each network. +async fn network_options( + client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != body.network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + Ok(Json(models::NetworkOptionsResponse { + version: models::Version { + rosetta_version: API_VERSION.to_string(), + node_version: status.version.version, + middleware_version: None, + metadata: None, + }, + allow: models::Allow { + operation_statuses: models::OperationStatusKind::iter() + .map(|status| models::OperationStatus { + status, + successful: status.is_successful(), + }) + .collect(), + operation_types: models::OperationType::iter().collect(), + errors: errors::ErrorKind::iter().map(models::Error::from_error_kind).collect(), + historical_balance_lookup: true, + }, + })) +} + +#[api_v2_operation] +/// Get a Block +/// +/// Get a block by its Block Identifier. If transactions are returned in the +/// same call to the node as fetching the block, the response should include +/// these transactions in the Block object. If not, an array of Transaction +/// Identifiers should be returned so /block/transaction fetches can be done to +/// get all transaction information. +/// +/// When requesting a block by the hash component of the BlockIdentifier, +/// this request MUST be idempotent: repeated invocations for the same +/// hash-identified block must return the exact same block contents. +/// +/// No such restriction is imposed when requesting a block by height, +/// given that a chain reorg event might cause the specific block at +/// height `n` to be set to a different one. +async fn block_details( + genesis: web::Data>, + client_addr: web::Data>, + view_client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + let Json(models::BlockRequest { network_identifier, block_identifier }) = body; + + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + // TODO: avoid ad-hoc error handling of common use-cases + let block_id: near_primitives::types::BlockReference = + block_identifier.try_into().map_err(|_| models::Error { + code: 4, + message: "Invalid input".to_string(), + retriable: true, + details: None, + })?; + + let block = match view_client_addr.send(near_client::GetBlock(block_id.clone())).await? { + Ok(block) => block, + Err(_) => return Ok(Json(models::BlockResponse { block: None, other_transactions: None })), + }; + + let block_identifier: models::BlockIdentifier = (&block.header).into(); + + let parent_block_identifier = if block.header.prev_hash == Default::default() { + // According to Rosetta API genesis block should have the parent block + // identifier referencing itself: + block_identifier.clone() + } else { + let parent_block = view_client_addr + .send(near_client::GetBlock( + near_primitives::types::BlockId::Hash(block.header.prev_hash).into(), + )) + .await? + .map_err(errors::ErrorKind::InternalError)?; + + models::BlockIdentifier { + index: parent_block.header.height.try_into().unwrap(), + hash: parent_block.header.hash.to_base(), + } + }; + + let transactions = crate::adapters::collect_transactions( + Arc::clone(&genesis), + Addr::clone(&view_client_addr), + &block, + ) + .await?; + + Ok(Json(models::BlockResponse { + block: Some(models::Block { + block_identifier, + parent_block_identifier, + timestamp: (block.header.timestamp / 1_000_000).try_into().unwrap(), + transactions, + metadata: None, + }), + other_transactions: None, + })) +} + +#[api_v2_operation] +/// Get a Block Transaction +/// +/// Get a transaction in a block by its Transaction Identifier. This endpoint +/// should only be used when querying a node for a block does not return all +/// transactions contained within it. All transactions returned by this endpoint +/// must be appended to any transactions returned by the /block method by +/// consumers of this data. Fetching a transaction by hash is considered an +/// Explorer Method (which is classified under the Future Work section). Calling +/// this endpoint requires reference to a BlockIdentifier because transaction +/// parsing can change depending on which block contains the transaction. For +/// example, in Bitcoin it is necessary to know which block contains a +/// transaction to determine the destination of fee payments. Without specifying +/// a block identifier, the node would have to infer which block to use (which +/// could change during a re-org). Implementations that require fetching +/// previous transactions to populate the response (ex: Previous UTXOs in +/// Bitcoin) may find it useful to run a cache within the Rosetta server in the +/// /data directory (on a path that does not conflict with the node). +/// +/// NOTE: The current implementation is suboptimal as it processes the whole +/// block to only return a single transaction. +async fn block_transaction_details( + genesis: web::Data>, + client_addr: web::Data>, + view_client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + let Json(models::BlockTransactionRequest { + network_identifier, + block_identifier, + transaction_identifier, + }) = body; + + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + // TODO: avoid ad-hoc error handling of common use-cases + let block_id: near_primitives::types::BlockReference = + block_identifier.try_into().map_err(|_| models::Error { + code: 4, + message: "Invalid input".to_string(), + retriable: true, + details: None, + })?; + + let block = view_client_addr + .send(near_client::GetBlock(block_id.clone())) + .await? + .map_err(errors::ErrorKind::NotFound)?; + + let transaction = crate::adapters::collect_transactions( + Arc::clone(&genesis), + Addr::clone(&view_client_addr), + &block, + ) + .await? + .into_iter() + .find(|transaction| transaction.transaction_identifier == transaction_identifier) + .ok_or_else(|| errors::ErrorKind::NotFound("Transaction not found".into()))?; + + Ok(Json(models::BlockTransactionResponse { transaction })) +} + +#[api_v2_operation] +/// Get an Account Balance +/// +/// Get an array of all AccountBalances for an AccountIdentifier and the +/// BlockIdentifier at which the balance lookup was performed. The +/// BlockIdentifier must always be returned because some consumers of account +/// balance data need to know specifically at which block the balance was +/// calculated to compare balances they compute from operations with the balance +/// returned by the node. It is important to note that making a balance request +/// for an account without populating the SubAccountIdentifier should not result +/// in the balance of all possible SubAccountIdentifiers being returned. Rather, +/// it should result in the balance pertaining to no SubAccountIdentifiers being +/// returned (sometimes called the liquid balance). To get all balances +/// associated with an account, it may be necessary to perform multiple balance +/// requests with unique AccountIdentifiers. It is also possible to perform a +/// historical balance lookup (if the server supports it) by passing in an +/// optional BlockIdentifier. +async fn account_balance( + genesis: web::Data>, + client_addr: web::Data>, + view_client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + let Json(models::AccountBalanceRequest { + network_identifier, + block_identifier, + account_identifier, + }) = body; + + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + // TODO: avoid ad-hoc error handling of common use-cases + let block_id: near_primitives::types::BlockReference = block_identifier + .map(TryInto::try_into) + .unwrap_or(Ok(near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::Final, + ))) + .map_err(|_| models::Error { + code: 4, + message: "Invalid input".to_string(), + retriable: true, + details: None, + })?; + + // TODO: update error handling once we return structured errors from the + // view_client handlers + let block = view_client_addr + .send(near_client::GetBlock(block_id.clone())) + .await? + .map_err(errors::ErrorKind::InternalError)?; + + let query = near_client::Query::new( + block_id, + near_primitives::views::QueryRequest::ViewAccount { + account_id: account_identifier.address, + }, + ); + let account_info_response = tokio::time::timeout(std::time::Duration::from_secs(10), async { + loop { + match view_client_addr.send(query.clone()).await? { + Ok(Some(query_response)) => return Ok(Some(query_response)), + Ok(None) => {} + // TODO: update this once we return structured errors from the view_client handlers + Err(err) => { + if err.contains("does not exist") { + return Ok(None); + } + return Err(models::Error::from(errors::ErrorKind::InternalError(err))); + } + } + tokio::time::delay_for(std::time::Duration::from_millis(100)).await; + } + }) + .await??; + + let (block_hash, block_height, account_info) = if let Some(account_info_response) = + account_info_response + { + let account_info = match account_info_response.kind { + near_primitives::views::QueryResponseKind::ViewAccount(account_info) => account_info, + _ => { + return Err(errors::ErrorKind::InternalInvariantError(format!( + "queried ViewAccount, but received {:?}.", + account_info_response.kind + )) + .into()); + } + }; + (account_info_response.block_hash, account_info_response.block_height, account_info) + } else { + ( + block.header.hash, + block.header.height, + near_primitives::account::Account { + amount: 0, + locked: 0, + storage_usage: 0, + code_hash: Default::default(), + } + .into(), + ) + }; + + let account_balances = crate::utils::RosettaAccountBalances::from_account( + account_info, + &genesis.config.runtime_config, + ); + + let balance = if let Some(sub_account) = account_identifier.sub_account { + match sub_account.address { + crate::models::SubAccount::Locked => account_balances.locked, + crate::models::SubAccount::LiquidBalanceForStorage => { + account_balances.liquid_for_storage + } + } + } else { + account_balances.liquid + }; + + Ok(Json(models::AccountBalanceResponse { + block_identifier: models::BlockIdentifier { + hash: block_hash.to_base(), + index: block_height.try_into().unwrap(), + }, + balances: vec![models::Amount { + value: balance.to_string(), + currency: consts::YOCTO_NEAR_CURRENCY.clone(), + metadata: None, + }], + metadata: None, + })) +} + +#[api_v2_operation] +/// Get All Mempool Transactions (not implemented) +/// +/// Get all Transaction Identifiers in the mempool +/// +/// NOTE: The mempool is short-lived, so it is currently not implemented. +async fn mempool( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + Ok(Json(models::MempoolResponse { transaction_identifiers: vec![] })) +} + +#[api_v2_operation] +/// Get a Mempool Transaction (not implemented) +/// +/// Get a transaction in the mempool by its Transaction Identifier. This is a +/// separate request than fetching a block transaction (/block/transaction) +/// because some blockchain nodes need to know that a transaction query is for +/// something in the mempool instead of a transaction in a block. Transactions +/// may not be fully parsable until they are in a block (ex: may not be possible +/// to determine the fee to pay before a transaction is executed). On this +/// endpoint, it is ok that returned transactions are only estimates of what may +/// actually be included in a block. +/// +/// NOTE: The mempool is short-lived, so this method does not make a lot of +/// sense to be implemented. +async fn mempool_transaction( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Derive an Address from a PublicKey (not implemented by design) +/// +/// Derive returns the network-specific address associated with a public key. +/// +/// Blockchains that require an on-chain action to create an account should not +/// implement this method. +async fn construction_derive() -> Result, models::Error> { + Err(errors::ErrorKind::InternalError("Not implemented by design".to_string()).into()) +} + +#[api_v2_operation] +/// Create a Request to Fetch Metadata (not implemented yet) +/// +/// Preprocess is called prior to /construction/payloads to construct a request +/// for any metadata that is needed for transaction construction given (i.e. +/// account nonce). The request returned from this method will be used by the +/// caller (in a different execution environment) to call the +/// /construction/metadata endpoint. +async fn construction_preprocess( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + // TODO + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Get Metadata for Transaction Construction (not implemented yet) +/// +/// Get any information required to construct a transaction for a specific +/// network. Metadata returned here could be a recent hash to use, an account +/// sequence number, or even arbitrary chain state. The request used when +/// calling this endpoint is often created by calling /construction/preprocess +/// in an offline environment. It is important to clarify that this endpoint +/// should not pre-construct any transactions for the client (this should happen +/// in /construction/payloads). This endpoint is left purposely unstructured +/// because of the wide scope of metadata that could be required. +async fn construction_metadata( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + // TODO + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Generate an Unsigned Transaction and Signing Payloads (not implemented yet) +/// +/// Payloads is called with an array of operations and the response from +/// `/construction/metadata`. It returns an unsigned transaction blob and a +/// collection of payloads that must be signed by particular addresses using a +/// certain SignatureType. The array of operations provided in transaction +/// construction often times can not specify all "effects" of a transaction +/// (consider invoked transactions in Ethereum). However, they can +/// deterministically specify the "intent" of the transaction, which is +/// sufficient for construction. For this reason, parsing the corresponding +/// transaction in the Data API (when it lands on chain) will contain a superset +/// of whatever operations were provided during construction. +async fn construction_payloads( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + // TODO + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Create Network Transaction from Signatures (not implemented yet) +/// +/// Combine creates a network-specific transaction from an unsigned transaction +/// and an array of provided signatures. The signed transaction returned from +/// this method will be sent to the /construction/submit endpoint by the caller. +async fn construction_combine( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + // TODO + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Parse a Transaction (not implemented yet) +/// +/// Parse is called on both unsigned and signed transactions to understand the +/// intent of the formulated transaction. This is run as a sanity check before +/// signing (after /construction/payloads) and before broadcast (after +/// /construction/combine). +async fn construction_parse( + _client_addr: web::Data>, + _body: Json, +) -> Result, models::Error> { + // TODO + Err(errors::ErrorKind::InternalError("Not implemented yet".to_string()).into()) +} + +#[api_v2_operation] +/// Get the Hash of a Signed Transaction +/// +/// TransactionHash returns the network-specific transaction hash for a signed +/// transaction. +async fn construction_hash( + body: Json, +) -> Result, models::Error> { + let Json(models::ConstructionHashRequest { network_identifier: _, signed_transaction }) = body; + + Ok(Json(models::TransactionIdentifierResponse { + transaction_identifier: models::TransactionIdentifier { + hash: signed_transaction.0.get_hash().to_base(), + }, + metadata: None, + })) +} + +#[api_v2_operation] +/// Submit a Signed Transaction +/// +/// Submit a pre-signed transaction to the node. This call should not block on +/// the transaction being included in a block. Rather, it should return +/// immediately with an indication of whether or not the transaction was +/// included in the mempool. The transaction submission response should only +/// return a 200 status if the submitted transaction could be included in the +/// mempool. Otherwise, it should return an error. +async fn construction_submit( + client_addr: web::Data>, + body: Json, +) -> Result, models::Error> { + let Json(models::ConstructionSubmitRequest { network_identifier, signed_transaction }) = body; + + // TODO: reduce copy-paste + let status = client_addr + .send(near_client::Status { is_health_check: false }) + .await? + .map_err(errors::ErrorKind::InternalError)?; + if status.chain_id != network_identifier.network { + return Err(models::Error { + code: 2, + message: "Wrong network (chain id)".to_string(), + retriable: true, + details: None, + }); + } + + let transaction_hash = signed_transaction.0.get_hash().to_base(); + let transaction_submittion = client_addr + .send(near_network::NetworkClientMessages::Transaction { + transaction: signed_transaction.0, + is_forwarded: false, + check_only: false, + }) + .await?; + match transaction_submittion { + near_network::NetworkClientResponses::ValidTx + | near_network::NetworkClientResponses::RequestRouted => { + Ok(Json(models::TransactionIdentifierResponse { + transaction_identifier: models::TransactionIdentifier { hash: transaction_hash }, + metadata: None, + })) + } + near_network::NetworkClientResponses::InvalidTx(error) => { + Err(errors::ErrorKind::InvalidInput(error.to_string()).into()) + } + _ => Err(errors::ErrorKind::InternalInvariantError(format!( + "Transaction submition return unexpected result: {:?}", + transaction_submittion + )) + .into()), + } +} + +fn get_cors(cors_allowed_origins: &[String]) -> CorsFactory { + let mut cors = Cors::new(); + if cors_allowed_origins != ["*".to_string()] { + for origin in cors_allowed_origins { + cors = cors.allowed_origin(&origin); + } + } + cors.allowed_methods(vec!["GET", "POST"]) + .allowed_headers(vec![ + actix_web::http::header::AUTHORIZATION, + actix_web::http::header::ACCEPT, + ]) + .allowed_header(actix_web::http::header::CONTENT_TYPE) + .max_age(3600) + .finish() +} + +pub fn start_rosetta_rpc( + config: crate::config::RosettaRpcConfig, + genesis: Arc, + client_addr: Addr, + view_client_addr: Addr, +) { + let crate::config::RosettaRpcConfig { addr, cors_allowed_origins } = config; + HttpServer::new(move || { + App::new() + .data(Arc::clone(&genesis)) + .data(client_addr.clone()) + .data(view_client_addr.clone()) + .wrap(get_cors(&cors_allowed_origins)) + .wrap_api() + .service(web::resource("/network/list").route(web::post().to(network_list))) + .service(web::resource("/network/status").route(web::post().to(network_status))) + .service(web::resource("/network/options").route(web::post().to(network_options))) + .service(web::resource("/block").route(web::post().to(block_details))) + .service( + web::resource("/block/transaction") + .route(web::post().to(block_transaction_details)), + ) + .service(web::resource("/account/balance").route(web::post().to(account_balance))) + .service( + web::resource("/construction/submit").route(web::post().to(construction_submit)), + ) + .service(web::resource("/mempool").route(web::post().to(mempool))) + .service( + web::resource("/mempool/transaction").route(web::post().to(mempool_transaction)), + ) + .service( + web::resource("/construction/derive").route(web::post().to(construction_derive)), + ) + .service( + web::resource("/construction/preprocess") + .route(web::post().to(construction_preprocess)), + ) + .service( + web::resource("/construction/metadata") + .route(web::post().to(construction_metadata)), + ) + .service( + web::resource("/construction/payloads") + .route(web::post().to(construction_payloads)), + ) + .service( + web::resource("/construction/combine").route(web::post().to(construction_combine)), + ) + .service(web::resource("/construction/parse").route(web::post().to(construction_parse))) + .service(web::resource("/construction/hash").route(web::post().to(construction_hash))) + .with_json_spec_at("/api/spec") + .build() + }) + .bind(addr) + .unwrap() + .run(); +} diff --git a/chain/rosetta-rpc/src/models.rs b/chain/rosetta-rpc/src/models.rs new file mode 100644 index 00000000000..44ec5b5412b --- /dev/null +++ b/chain/rosetta-rpc/src/models.rs @@ -0,0 +1,842 @@ +use std::convert::{TryFrom, TryInto}; + +use paperclip::actix::{api_v2_errors, Apiv2Schema}; + +use near_primitives::borsh::{BorshDeserialize, BorshSerialize}; +use near_primitives::serialize::BaseEncode; + +/// An AccountBalanceRequest is utilized to make a balance request on the +/// /account/balance endpoint. If the block_identifier is populated, a +/// historical balance query should be performed. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct AccountBalanceRequest { + pub network_identifier: NetworkIdentifier, + + pub account_identifier: AccountIdentifier, + + #[serde(skip_serializing_if = "Option::is_none")] + pub block_identifier: Option, +} + +/// An AccountBalanceResponse is returned on the /account/balance endpoint. If +/// an account has a balance for each AccountIdentifier describing it (ex: an +/// ERC-20 token balance on a few smart contracts), an account balance request +/// must be made with each AccountIdentifier. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +#[cfg_attr(feature = "conversion", derive(frunk::LabelledGeneric))] +pub(crate) struct AccountBalanceResponse { + pub block_identifier: BlockIdentifier, + + /// A single account may have a balance in multiple currencies. + pub balances: Vec, + + /// Account-based blockchains that utilize a nonce or sequence number should + /// include that number in the metadata. This number could be unique to the + /// identifier or global across the account address. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// The account_identifier uniquely identifies an account within a network. All +/// fields in the account_identifier are utilized to determine this uniqueness +/// (including the metadata field, if populated). +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct AccountIdentifier { + /// The address may be a cryptographic public key (or some encoding of it) + /// or a provided username. + pub address: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sub_account: Option, + + /// Blockchains that utilize a username model (where the address is not a + /// derivative of a cryptographic public key) should specify the public + /// key(s) owned by the address in metadata. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// Allow specifies supported Operation status, Operation types, and all +/// possible error statuses. This Allow object is used by clients to validate +/// the correctness of a Rosetta Server implementation. It is expected that +/// these clients will error if they receive some response that contains any of +/// the above information that is not specified here. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Allow { + /// All Operation.Status this implementation supports. Any status that is + /// returned during parsing that is not listed here will cause client + /// validation to error. + pub operation_statuses: Vec, + + /// All Operation.Type this implementation supports. Any type that is + /// returned during parsing that is not listed here will cause client + /// validation to error. + pub operation_types: Vec, + + /// All Errors that this implementation could return. Any error that is + /// returned during parsing that is not listed here will cause client + /// validation to error. + pub errors: Vec, + + /// Any Rosetta implementation that supports querying the balance of an + /// account at any height in the past should set this to true. + pub historical_balance_lookup: bool, +} + +/// Amount is some Value of a Currency. It is considered invalid to specify a +/// Value without a Currency. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Amount { + /// Value of the transaction in atomic units represented as an + /// arbitrary-sized signed integer. For example, 1 BTC would be represented + /// by a value of 100000000. + pub value: String, + + pub currency: Currency, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +impl Amount { + pub(crate) fn from_yoctonear(amount: near_primitives::types::Balance) -> Self { + Self { + value: amount.to_string(), + currency: crate::consts::YOCTO_NEAR_CURRENCY.clone(), + metadata: None, + } + } + + pub(crate) fn from_yoctonear_diff( + amount: crate::utils::SignedDiff, + ) -> Self { + Self { + value: amount.to_string(), + currency: crate::consts::YOCTO_NEAR_CURRENCY.clone(), + metadata: None, + } + } +} + +/// Blocks contain an array of Transactions that occurred at a particular +/// BlockIdentifier. A hard requirement for blocks returned by Rosetta +/// implementations is that they MUST be _inalterable_: once a client has +/// requested and received a block identified by a specific BlockIndentifier, +/// all future calls for that same BlockIdentifier must return the same block +/// contents. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Block { + pub block_identifier: BlockIdentifier, + + pub parent_block_identifier: BlockIdentifier, + + /// The timestamp of the block in milliseconds since the Unix Epoch. The + /// timestamp is stored in milliseconds because some blockchains produce + /// blocks more often than once a second. + pub timestamp: i64, + + pub transactions: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// The block_identifier uniquely identifies a block in a particular network. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct BlockIdentifier { + /// This is also known as the block height. + pub index: i64, + + pub hash: String, +} + +impl From<&near_primitives::views::BlockHeaderView> for BlockIdentifier { + fn from(header: &near_primitives::views::BlockHeaderView) -> Self { + Self { + index: header + .height + .try_into() + .expect("Rosetta only supports block indecies up to i64::MAX"), + hash: header.hash.to_base(), + } + } +} + +/// A BlockRequest is utilized to make a block request on the /block endpoint. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct BlockRequest { + pub network_identifier: NetworkIdentifier, + + pub block_identifier: PartialBlockIdentifier, +} + +/// A BlockResponse includes a fully-populated block or a partially-populated +/// block with a list of other transactions to fetch (other_transactions). +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct BlockResponse { + pub block: Option, + + /// Some blockchains may require additional transactions to be fetched that + /// weren't returned in the block response (ex: block only returns + /// transaction hashes). For blockchains with a lot of transactions in each + /// block, this can be very useful as consumers can concurrently fetch all + /// transactions returned. + #[serde(skip_serializing_if = "Option::is_none")] + pub other_transactions: Option>, +} + +/// A BlockTransactionRequest is used to fetch a Transaction included in a block +/// that is not returned in a BlockResponse. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct BlockTransactionRequest { + pub network_identifier: NetworkIdentifier, + + pub block_identifier: PartialBlockIdentifier, + + pub transaction_identifier: TransactionIdentifier, +} + +/// A BlockTransactionResponse contains information about a block transaction. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct BlockTransactionResponse { + pub transaction: Transaction, +} + +/// A ConstructionMetadataRequest is utilized to get information required to +/// construct a transaction. The Options object used to specify which metadata +/// to return is left purposely unstructured to allow flexibility for +/// implementers. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct ConstructionMetadataRequest { + pub network_identifier: NetworkIdentifier, + + /// Some blockchains require different metadata for different types of + /// transaction construction (ex: delegation versus a transfer). Instead of + /// requiring a blockchain node to return all possible types of metadata for + /// construction (which may require multiple node fetches), the client can + /// populate an options object to limit the metadata returned to only the + /// subset required. + pub options: serde_json::Value, +} + +/// The ConstructionMetadataResponse returns network-specific metadata used for +/// transaction construction. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct ConstructionMetadataResponse { + pub metadata: serde_json::Value, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct JsonSignedTransaction(pub near_primitives::transaction::SignedTransaction); + +impl paperclip::v2::schema::TypedData for JsonSignedTransaction { + fn data_type() -> paperclip::v2::models::DataType { + paperclip::v2::models::DataType::String + } +} + +impl serde::Serialize for JsonSignedTransaction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&near_primitives::serialize::to_base64( + self.0.try_to_vec().expect("borsh serialization should never fail"), + )) + } +} + +impl<'de> serde::Deserialize<'de> for JsonSignedTransaction { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let raw_signed_transaction = near_primitives::serialize::from_base64( + &::deserialize(deserializer)?, + ) + .map_err(|err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other(&format!( + "signed transaction could not be decoded due to: {:?}", + err + )), + &"base64-encoded transaction was expected", + ) + })?; + Ok(Self( + near_primitives::transaction::SignedTransaction::try_from_slice( + &raw_signed_transaction, + ) + .map_err(|err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other(&format!( + "signed transaction could not be deserialized due to: {:?}", + err + )), + &"a valid Borsh-serialized transaction was expected", + ) + })?, + )) + } +} + +/// The transaction submission request includes a signed transaction. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct ConstructionSubmitRequest { + pub network_identifier: NetworkIdentifier, + + pub signed_transaction: JsonSignedTransaction, +} + +/// TransactionIdentifierResponse contains the transaction_identifier of a +/// transaction that was submitted to either `/construction/hash` or +/// `/construction/submit`. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct TransactionIdentifierResponse { + pub transaction_identifier: TransactionIdentifier, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// ConstructionHashRequest is the input to the /construction/hash endpoint. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct ConstructionHashRequest { + pub network_identifier: NetworkIdentifier, + + pub signed_transaction: JsonSignedTransaction, +} + +/// Currency is composed of a canonical Symbol and Decimals. This Decimals value +/// is used to convert an Amount.Value from atomic units (Satoshis) to standard +/// units (Bitcoins). +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Currency { + /// Canonical symbol associated with a currency. + pub symbol: String, + + /// Number of decimal places in the standard unit representation of the + /// amount. For example, BTC has 8 decimals. Note that it is not possible + /// to represent the value of some currency in atomic units that is not base + /// 10. + pub decimals: u32, + + /// Any additional information related to the currency itself. For example, + /// it would be useful to populate this object with the contract address of + /// an ERC-20 token. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// Instead of utilizing HTTP status codes to describe node errors (which often +/// do not have a good analog), rich errors are returned using this object. +#[api_v2_errors(code = 500, description = "See the inner `code` value to get more details")] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Error { + /// Code is a network-specific error code. If desired, this code can be + /// equivalent to an HTTP status code. + pub code: u32, + + /// Message is a network-specific error message. + pub message: String, + + /// An error is retriable if the same request may succeed if submitted + /// again. + pub retriable: bool, + + /// Often times it is useful to return context specific to the request that + /// caused the error (i.e. a sample of the stack trace or impacted account) + /// in addition to the standard error message. + #[serde(skip_serializing_if = "Option::is_none")] + pub details: Option, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let retriable = if self.retriable { " (retriable)" } else { "" }; + write!(f, "Error #{}{}: {}", self.code, retriable, self.message) + } +} + +impl Error { + pub(crate) fn from_error_kind(err: crate::errors::ErrorKind) -> Self { + match err { + crate::errors::ErrorKind::InvalidInput(message) => Self { + code: 400, + message: format!("Invalid Input: {}", message), + retriable: false, + details: None, + }, + crate::errors::ErrorKind::NotFound(message) => Self { + code: 404, + message: format!("Not Found: {}", message), + retriable: false, + details: None, + }, + crate::errors::ErrorKind::WrongNetwork(message) => Self { + code: 403, + message: format!("Wrong Network: {}", message), + retriable: false, + details: None, + }, + crate::errors::ErrorKind::Timeout(message) => Self { + code: 504, + message: format!("Timeout: {}", message), + retriable: true, + details: None, + }, + crate::errors::ErrorKind::InternalInvariantError(message) => Self { + code: 501, + message: format!("Internal Invariant Error (please, report it): {}", message), + retriable: true, + details: None, + }, + crate::errors::ErrorKind::InternalError(message) => Self { + code: 500, + message: format!("Internal Error: {}", message), + retriable: true, + details: None, + }, + } + } +} + +impl std::convert::From for Error +where + T: Into, +{ + fn from(err: T) -> Self { + Self::from_error_kind(err.into()) + } +} + +impl actix_web::ResponseError for Error { + fn error_response(&self) -> actix_web::HttpResponse { + let data = paperclip::actix::web::Json(self).clone(); + actix_web::HttpResponse::build(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR) + .json(data) + } +} + +/// A MempoolResponse contains all transaction identifiers in the mempool for a +/// particular network_identifier. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct MempoolResponse { + pub transaction_identifiers: Vec, +} + +/// A MempoolTransactionRequest is utilized to retrieve a transaction from the +/// mempool. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct MempoolTransactionRequest { + pub network_identifier: NetworkIdentifier, + + pub transaction_identifier: TransactionIdentifier, +} + +/// A MempoolTransactionResponse contains an estimate of a mempool transaction. +/// It may not be possible to know the full impact of a transaction in the +/// mempool (ex: fee paid). +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct MempoolTransactionResponse { + pub transaction: Transaction, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// A MetadataRequest is utilized in any request where the only argument is +/// optional metadata. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct MetadataRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// The network_identifier specifies which network a particular object is +/// associated with. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct NetworkIdentifier { + pub blockchain: String, + + /// If a blockchain has a specific chain-id or network identifier, it should + /// go in this field. It is up to the client to determine which + /// network-specific identifier is mainnet or testnet. + pub network: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sub_network_identifier: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub(crate) enum SyncStage { + AwaitingPeers, + NoSync, + HeaderSync, + StateSync, + StateSyncDone, + BodySync, +} + +/// SyncStatus is used to provide additional context about an implementation's +/// sync status. It is often used to indicate that an implementation is healthy +/// when it cannot be queried until some sync phase occurs. If an +/// implementation is immediately queryable, this model is often not populated. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct SyncStatus { + pub current_index: i64, + + #[serde(skip_serializing_if = "Option::is_none")] + pub target_index: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub stage: Option, +} + +/// A NetworkListResponse contains all NetworkIdentifiers that the node can +/// serve information for. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct NetworkListResponse { + pub network_identifiers: Vec, +} + +/// NetworkOptionsResponse contains information about the versioning of the node +/// and the allowed operation statuses, operation types, and errors. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct NetworkOptionsResponse { + pub version: Version, + + pub allow: Allow, +} + +/// A NetworkRequest is utilized to retrieve some data specific exclusively to a +/// NetworkIdentifier. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct NetworkRequest { + pub network_identifier: NetworkIdentifier, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// NetworkStatusResponse contains basic information about the node's view of a +/// blockchain network. If a Rosetta implementation prunes historical state, it +/// should populate the optional `oldest_block_identifier` field with the oldest +/// block available to query. If this is not populated, it is assumed that the +/// `genesis_block_identifier` is the oldest queryable block. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct NetworkStatusResponse { + pub current_block_identifier: BlockIdentifier, + + /// The timestamp of the block in milliseconds since the Unix Epoch. The + /// timestamp is stored in milliseconds because some blockchains produce + /// blocks more often than once a second. + pub current_block_timestamp: i64, + + pub genesis_block_identifier: BlockIdentifier, + + pub oldest_block_identifier: BlockIdentifier, + + #[serde(skip_serializing_if = "Option::is_none")] + pub sync_status: Option, + + pub peers: Vec, +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + serde::Serialize, + serde::Deserialize, + Apiv2Schema, + strum::EnumIter, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub(crate) enum OperationType { + CreateAccount, + DeployContract, + FunctionCall, + Transfer, + Stake, + AddKey, + DeleteKey, + DeleteAccount, +} + +impl std::convert::From<&near_primitives::views::ActionView> for OperationType { + fn from(action: &near_primitives::views::ActionView) -> Self { + match action { + near_primitives::views::ActionView::CreateAccount => Self::CreateAccount, + near_primitives::views::ActionView::DeployContract { .. } => Self::DeployContract, + near_primitives::views::ActionView::FunctionCall { .. } => Self::FunctionCall, + near_primitives::views::ActionView::Transfer { .. } => Self::Transfer, + near_primitives::views::ActionView::Stake { .. } => Self::Stake, + near_primitives::views::ActionView::AddKey { .. } => Self::AddKey, + near_primitives::views::ActionView::DeleteKey { .. } => Self::DeleteKey, + near_primitives::views::ActionView::DeleteAccount { .. } => Self::DeleteAccount, + } + } +} + +#[derive( + Debug, + Clone, + Copy, + PartialEq, + serde::Serialize, + serde::Deserialize, + Apiv2Schema, + strum::EnumIter, +)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub(crate) enum OperationStatusKind { + Success, +} + +impl OperationStatusKind { + pub(crate) fn is_successful(&self) -> bool { + match self { + Self::Success => true, + } + } +} + +/// Operations contain all balance-changing information within a transaction. +/// They are always one-sided (only affect 1 AccountIdentifier) and can +/// succeed or fail independently from a Transaction. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Operation { + pub operation_identifier: OperationIdentifier, + + /// Restrict referenced related_operations to identifier indexes < the + /// current operation_identifier.index. This ensures there exists a clear + /// DAG-structure of relations. Since operations are one-sided, one could + /// imagine relating operations in a single transfer or linking operations + /// in a call tree. + #[serde(skip_serializing_if = "Option::is_none")] + pub related_operations: Option>, + + /// The network-specific type of the operation. Ensure that any type that + /// can be returned here is also specified in the NetworkStatus. This can + /// be very useful to downstream consumers that parse all block data. + #[serde(rename = "type")] + pub type_: OperationType, + + /// The network-specific status of the operation. Status is not defined on + /// the transaction object because blockchains with smart contracts may have + /// transactions that partially apply. Blockchains with atomic transactions + /// (all operations succeed or all operations fail) will have the same + /// status for each operation. + pub status: OperationStatusKind, + + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub amount: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// The operation_identifier uniquely identifies an operation within a +/// transaction. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct OperationIdentifier { + /// The operation index is used to ensure each operation has a unique + /// identifier within a transaction. This index is only relative to the + /// transaction and NOT GLOBAL. The operations in each transaction should + /// start from index 0. To clarify, there may not be any notion of an + /// operation index in the blockchain being described. + pub index: i64, + + /// Some blockchains specify an operation index that is essential for + /// client use. For example, Bitcoin uses a network_index to identify + /// which UTXO was used in a transaction. network_index should not be + /// populated if there is no notion of an operation index in a blockchain + /// (typically most account-based blockchains). + #[serde(skip_serializing_if = "Option::is_none")] + pub network_index: Option, +} + +impl OperationIdentifier { + pub(crate) fn new(operations: &[Operation]) -> Self { + Self { + index: operations + .len() + .try_into() + .expect("there cannot be more than i64::MAX operations in a single transaction"), + network_index: None, + } + } +} + +/// OperationStatus is utilized to indicate which Operation status are +/// considered successful. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct OperationStatus { + /// The status is the network-specific status of the operation. + pub status: OperationStatusKind, + + /// An Operation is considered successful if the Operation.Amount should + /// affect the Operation.Account. Some blockchains (like Bitcoin) only + /// include successful operations in blocks but other blockchains (like + /// Ethereum) include unsuccessful operations that incur a fee. To + /// reconcile the computed balance from the stream of Operations, it is + /// critical to understand which Operation.Status indicate an Operation is + /// successful and should affect an Account. + pub successful: bool, +} + +/// When fetching data by BlockIdentifier, it may be possible to only specify +/// the index or hash. If neither property is specified, it is assumed that the +/// client is making a request at the current block. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct PartialBlockIdentifier { + #[serde(skip_serializing_if = "Option::is_none")] + index: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + hash: Option, +} + +impl TryFrom for near_primitives::types::BlockReference { + type Error = (); + + fn try_from(block_identifier: PartialBlockIdentifier) -> Result { + Ok(match (block_identifier.index, block_identifier.hash) { + (Some(index), None) => { + near_primitives::types::BlockId::Height(index.try_into().map_err(|_| ())?).into() + } + (_, Some(hash)) => { + near_primitives::types::BlockId::Hash(hash.try_into().map_err(|_| ())?).into() + } + (None, None) => near_primitives::types::BlockReference::Finality( + near_primitives::types::Finality::Final, + ), + }) + } +} + +/// A Peer is a representation of a node's peer. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Peer { + pub peer_id: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub(crate) enum SubAccount { + LiquidBalanceForStorage, + Locked, +} + +impl From for crate::models::SubAccountIdentifier { + fn from(sub_account: SubAccount) -> Self { + crate::models::SubAccountIdentifier { address: sub_account, metadata: None } + } +} + +/// An account may have state specific to a contract address (ERC-20 token) +/// and/or a stake (delegated balance). The sub_account_identifier should +/// specify which state (if applicable) an account instantiation refers to. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct SubAccountIdentifier { + /// The SubAccount address may be a cryptographic value or some other + /// identifier (ex: bonded) that uniquely specifies a SubAccount. + pub address: SubAccount, + + /// If the SubAccount address is not sufficient to uniquely specify a + /// SubAccount, any other identifying information can be stored here. It is + /// important to note that two SubAccounts with identical addresses but + /// differing metadata will not be considered equal by clients. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// In blockchains with sharded state, the SubNetworkIdentifier is required to +/// query some object on a specific shard. This identifier is optional for all +/// non-sharded blockchains. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct SubNetworkIdentifier { + pub network: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +/// The timestamp of the block in milliseconds since the Unix Epoch. The +/// timestamp is stored in milliseconds because some blockchains produce blocks +/// more often than once a second. +#[derive(Debug, Clone, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Timestamp(i64); + +/// Transactions contain an array of Operations that are attributable to the +/// same TransactionIdentifier. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Transaction { + pub transaction_identifier: TransactionIdentifier, + + pub operations: Vec, + + /// Transactions that are related to other transactions (like a cross-shard + /// transaction) should include the transaction_identifier of these + /// transactions in the metadata. + pub metadata: TransactionMetadata, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub(crate) enum TransactionType { + Block, + Transaction, + ActionReceipt, + DataReceipt, +} + +/// Extra data for Transaction +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct TransactionMetadata { + #[serde(rename = "type")] + pub(crate) type_: TransactionType, +} + +/// The transaction_identifier uniquely identifies a transaction in a particular +/// network and block or in the mempool. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct TransactionIdentifier { + /// Any transactions that are attributable only to a block (ex: a block + /// event) should use the hash of the block as the identifier. + pub hash: String, +} + +/// The Version object is utilized to inform the client of the versions of +/// different components of the Rosetta implementation. +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Apiv2Schema)] +pub(crate) struct Version { + /// The rosetta_version is the version of the Rosetta interface the + /// implementation adheres to. This can be useful for clients looking to + /// reliably parse responses. + pub rosetta_version: String, + + /// The node_version is the canonical version of the node runtime. This can + /// help clients manage deployments. + pub node_version: String, + + /// When a middleware server is used to adhere to the Rosetta interface, it + /// should return its version here. This can help clients manage + /// deployments. + #[serde(skip_serializing_if = "Option::is_none")] + pub middleware_version: Option, + + /// Any other information that may be useful about versioning of dependent + /// services should be returned here. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} diff --git a/chain/rosetta-rpc/src/utils.rs b/chain/rosetta-rpc/src/utils.rs new file mode 100644 index 00000000000..65987787e40 --- /dev/null +++ b/chain/rosetta-rpc/src/utils.rs @@ -0,0 +1,157 @@ +use actix::Addr; +use futures::StreamExt; + +use near_client::ViewClientActor; + +#[derive(Debug, Clone)] +pub(crate) struct SignedDiff +where + T: std::ops::Sub + std::cmp::PartialOrd + std::fmt::Display, +{ + is_positive: bool, + absolute_difference: T, +} + +impl SignedDiff +where + T: std::ops::Sub + std::cmp::Ord + std::fmt::Display, +{ + pub fn cmp(lhs: T, rhs: T) -> Self { + if lhs <= rhs { + Self { is_positive: true, absolute_difference: rhs - lhs } + } else { + Self { is_positive: false, absolute_difference: lhs - rhs } + } + } +} + +impl std::fmt::Display for SignedDiff +where + T: std::ops::Sub + std::cmp::Ord + std::fmt::Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}", if self.is_positive { "" } else { "-" }, self.absolute_difference) + } +} + +impl std::ops::Neg for SignedDiff +where + T: std::ops::Sub + std::cmp::Ord + std::fmt::Display, +{ + type Output = Self; + + fn neg(mut self) -> Self::Output { + self.is_positive = !self.is_positive; + self + } +} + +fn get_liquid_balance_for_storage( + mut account: near_primitives::account::Account, + runtime_config: &near_runtime_configs::RuntimeConfig, +) -> near_primitives::types::Balance { + account.amount = 0; + near_runtime_configs::get_insufficient_storage_stake(&account, &runtime_config) + .expect("get_insufficient_storage_stake never fails when state is consistent") + .unwrap_or(0) +} + +pub(crate) struct RosettaAccountBalances { + pub liquid: near_primitives::types::Balance, + pub liquid_for_storage: near_primitives::types::Balance, + pub locked: near_primitives::types::Balance, +} + +impl RosettaAccountBalances { + pub fn zero() -> Self { + Self { liquid: 0, liquid_for_storage: 0, locked: 0 } + } + + pub fn from_account>( + account: T, + runtime_config: &near_runtime_configs::RuntimeConfig, + ) -> Self { + let account = account.into(); + let amount = account.amount; + let locked = account.locked; + let liquid_for_storage = get_liquid_balance_for_storage(account, runtime_config); + + Self { + liquid_for_storage, + liquid: amount + .checked_sub(liquid_for_storage) + .expect("liquid balance for storage cannot be bigger than the total balance"), + locked, + } + } +} + +pub(crate) async fn query_accounts( + account_ids: impl Iterator, + block_id: &near_primitives::types::BlockReference, + view_client_addr: &Addr, +) -> Result< + std::collections::HashMap< + near_primitives::types::AccountId, + near_primitives::views::AccountView, + >, + crate::errors::ErrorKind, +> { + account_ids + .map(|account_id| { + async move { + let query = near_client::Query::new( + block_id.clone(), + near_primitives::views::QueryRequest::ViewAccount { + account_id: account_id.clone(), + }, + ); + let account_info_response = + tokio::time::timeout(std::time::Duration::from_secs(10), async { + loop { + match view_client_addr.send(query.clone()).await? { + Ok(Some(query_response)) => return Ok(Some(query_response)), + Ok(None) => {} + // TODO: update this once we return structured errors in the + // view_client handlers + Err(err) => { + if err.contains("does not exist") { + return Ok(None); + } + return Err(crate::errors::ErrorKind::InternalError(err)); + } + } + tokio::time::delay_for(std::time::Duration::from_millis(100)).await; + } + }) + .await??; + + let kind = if let Some(account_info_response) = account_info_response { + account_info_response.kind + } else { + return Ok(None); + }; + + match kind { + near_primitives::views::QueryResponseKind::ViewAccount(account_info) => { + Ok(Some((account_id.clone(), account_info))) + } + _ => Err(crate::errors::ErrorKind::InternalInvariantError( + "queried ViewAccount, but received something else.".to_string(), + ) + .into()), + } + } + }) + .collect::>() + .collect::, + crate::errors::ErrorKind, + >, + >>() + .await + .into_iter() + .filter_map(|account_info| account_info.transpose()) + .collect() +} diff --git a/core/primitives/src/types.rs b/core/primitives/src/types.rs index d82fb3e242b..f4732b92a0c 100644 --- a/core/primitives/src/types.rs +++ b/core/primitives/src/types.rs @@ -505,6 +505,18 @@ impl BlockReference { } } +impl From for BlockReference { + fn from(block_id: BlockId) -> Self { + Self::BlockId(block_id) + } +} + +impl From for BlockReference { + fn from(finality: Finality) -> Self { + Self::Finality(finality) + } +} + #[derive(Default, BorshSerialize, BorshDeserialize, Serialize, Clone, Debug, PartialEq)] pub struct ValidatorStats { pub produced: NumBlocks, diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 9f76487f91c..4e2b8de85b0 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -59,8 +59,8 @@ pub struct AccountView { pub storage_paid_at: BlockHeight, } -impl From for AccountView { - fn from(account: Account) -> Self { +impl From<&Account> for AccountView { + fn from(account: &Account) -> Self { AccountView { amount: account.amount, locked: account.locked, @@ -71,8 +71,14 @@ impl From for AccountView { } } -impl From for Account { - fn from(view: AccountView) -> Self { +impl From for AccountView { + fn from(account: Account) -> Self { + (&account).into() + } +} + +impl From<&AccountView> for Account { + fn from(view: &AccountView) -> Self { Self { amount: view.amount, locked: view.locked, @@ -82,6 +88,12 @@ impl From for Account { } } +impl From for Account { + fn from(view: AccountView) -> Self { + (&view).into() + } +} + #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub enum AccessKeyPermissionView { FunctionCall { diff --git a/core/runtime-configs/src/lib.rs b/core/runtime-configs/src/lib.rs index 9aa03768bac..80d4ad138e5 100644 --- a/core/runtime-configs/src/lib.rs +++ b/core/runtime-configs/src/lib.rs @@ -1,6 +1,7 @@ //! Settings of the parameters of the runtime. use serde::{Deserialize, Serialize}; +use near_primitives::account::Account; use near_primitives::serialize::u128_dec_format; use near_primitives::types::{AccountId, Balance}; use near_runtime_fees::RuntimeFeesConfig; @@ -65,6 +66,34 @@ impl Default for AccountCreationConfig { } } +/// Checks if given account has enough balance for storage stake, and returns: +/// - None if account has enough balance, +/// - Some(insufficient_balance) if account doesn't have enough and how much need to be added, +/// - Err(message) if account has invalid storage usage or amount/locked. +/// +/// Read details of state staking https://nomicon.io/Economics/README.html#state-stake +pub fn get_insufficient_storage_stake( + account: &Account, + runtime_config: &RuntimeConfig, +) -> Result, String> { + let required_amount = Balance::from(account.storage_usage) + .checked_mul(runtime_config.storage_amount_per_byte) + .ok_or_else(|| { + format!("Account's storage_usage {} overflows multiplication", account.storage_usage) + })?; + let available_amount = account.amount.checked_add(account.locked).ok_or_else(|| { + format!( + "Account's amount {} and locked {} overflow addition", + account.amount, account.locked + ) + })?; + if available_amount >= required_amount { + Ok(None) + } else { + Ok(Some(required_amount - available_amount)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/deny.toml b/deny.toml index 07d1d63ea11..aba92b92ae0 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,9 @@ deny = [ skip = [ # actix 0.9.0 still uses it { name = "tokio-util", version = "=0.2.0" }, + { name = "parking_lot", version = "=0.10.2" }, + { name = "parking_lot_core", version = "=0.7.2" }, + { name = "lock_api", version = "=0.3.4" }, # actix-server 1.0.2 still uses it { name = "miow", version = "=0.2.1" }, # miow 0.6.21 still uses it @@ -34,4 +37,10 @@ skip = [ { name = "wasmparser", version = "=0.51.4"}, { name = "itertools", version = "=0.8.2" }, { name = "rand_core", version = "=0.4.2" }, + + # rosetta-rpc pull paperclip which introduce the following duplocates (https://github.com/wafflespeanut/paperclip/pull/209) + { name = "url", version = "=1.7.2" }, + { name = "idna", version = "=0.1.5" }, + { name = "semver", version = "=0.9.0" }, + { name = "percent-encoding", version = "=1.0.1" }, ] diff --git a/neard/Cargo.toml b/neard/Cargo.toml index 7e2edbfc75d..de71cd5a66a 100644 --- a/neard/Cargo.toml +++ b/neard/Cargo.toml @@ -40,6 +40,7 @@ near-client = { path = "../chain/client" } near-pool = { path = "../chain/pool" } near-network = { path = "../chain/network" } near-jsonrpc = { path = "../chain/jsonrpc" } +near-rosetta-rpc = { path = "../chain/rosetta-rpc", optional = true } near-telemetry = { path = "../chain/telemetry" } near-epoch-manager = { path = "../chain/epoch_manager" } @@ -56,6 +57,7 @@ expensive_tests = ["near-client/expensive_tests", "near-epoch-manager/expensive_ metric_recorder = ["near-network/metric_recorder", "near-client/metric_recorder"] no_cache = ["node-runtime/no_cache", "near-store/no_cache", "near-chain/no_cache"] delay_detector = ["near-client/delay_detector"] +rosetta_rpc = ["near-rosetta-rpc"] [[bin]] path = "src/main.rs" diff --git a/neard/src/config.rs b/neard/src/config.rs index d6c272b7d6a..841406c398e 100644 --- a/neard/src/config.rs +++ b/neard/src/config.rs @@ -31,6 +31,8 @@ use near_primitives::types::{ use near_primitives::utils::{generate_random_string, get_num_seats_per_shard}; use near_primitives::validator_signer::{InMemoryValidatorSigner, ValidatorSigner}; use near_primitives::version::PROTOCOL_VERSION; +#[cfg(feature = "rosetta_rpc")] +use near_rosetta_rpc::RosettaRpcConfig; use near_runtime_configs::RuntimeConfig; use near_telemetry::TelemetryConfig; @@ -382,6 +384,8 @@ pub struct Config { pub validator_key_file: String, pub node_key_file: String, pub rpc: RpcConfig, + #[cfg(feature = "rosetta_rpc")] + pub rosetta_rpc: RosettaRpcConfig, pub telemetry: TelemetryConfig, pub network: Network, pub consensus: Consensus, @@ -402,6 +406,8 @@ impl Default for Config { validator_key_file: VALIDATOR_KEY_FILE.to_string(), node_key_file: NODE_KEY_FILE.to_string(), rpc: RpcConfig::default(), + #[cfg(feature = "rosetta_rpc")] + rosetta_rpc: RosettaRpcConfig::default(), telemetry: TelemetryConfig::default(), network: Network::default(), consensus: Consensus::default(), @@ -523,6 +529,8 @@ pub struct NearConfig { pub client_config: ClientConfig, pub network_config: NetworkConfig, pub rpc_config: RpcConfig, + #[cfg(feature = "rosetta_rpc")] + pub rosetta_rpc_config: RosettaRpcConfig, pub telemetry_config: TelemetryConfig, pub genesis: Arc, pub validator_signer: Option>, @@ -618,6 +626,8 @@ impl NearConfig { }, telemetry_config: config.telemetry, rpc_config: config.rpc, + #[cfg(feature = "rosetta_rpc")] + rosetta_rpc_config: config.rosetta_rpc, genesis, validator_signer, } diff --git a/neard/src/lib.rs b/neard/src/lib.rs index 737724bc79b..60209d0521c 100644 --- a/neard/src/lib.rs +++ b/neard/src/lib.rs @@ -12,6 +12,8 @@ use near_client::AdversarialControls; use near_client::{start_client, start_view_client, ClientActor, ViewClientActor}; use near_jsonrpc::start_http; use near_network::{NetworkRecipient, PeerManagerActor}; +#[cfg(feature = "rosetta_rpc")] +use near_rosetta_rpc::start_rosetta_rpc; use near_store::migrations::{ fill_col_outcomes_by_hash, fill_col_transaction_refcount, get_store_version, migrate_6_to_7, migrate_7_to_8, set_store_version, @@ -188,6 +190,13 @@ pub fn start_with_config( client_actor.clone(), view_client.clone(), ); + #[cfg(feature = "rosetta_rpc")] + start_rosetta_rpc( + config.rosetta_rpc_config, + Arc::clone(&config.genesis), + client_actor.clone(), + view_client.clone(), + ); config.network_config.verify(); diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index ececa0e4e32..de73ab3bb32 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -11,7 +11,7 @@ use near_primitives::transaction::{ Action, AddKeyAction, DeleteAccountAction, DeleteKeyAction, DeployContractAction, FunctionCallAction, StakeAction, TransferAction, }; -use near_primitives::types::{AccountId, Balance, EpochInfoProvider, ValidatorStake}; +use near_primitives::types::{AccountId, EpochInfoProvider, ValidatorStake}; use near_primitives::utils::{ is_valid_account_id, is_valid_sub_account_id, is_valid_top_level_account_id, }; @@ -33,37 +33,6 @@ use near_runtime_configs::AccountCreationConfig; use near_vm_errors::{CompilationError, FunctionCallError}; use near_vm_runner::VMError; -/// Checks if given account has enough balance for state stake, and returns: -/// - None if account has enough balance, -/// - Some(insufficient_balance) if account doesn't have enough and how much need to be added, -/// - Err(StorageError::StorageInconsistentState) if account has invalid storage usage or amount/locked. -/// -/// Read details of state staking https://nomicon.io/Economics/README.html#state-stake -pub(crate) fn get_insufficient_storage_stake( - account: &Account, - runtime_config: &RuntimeConfig, -) -> Result, StorageError> { - let required_amount = Balance::from(account.storage_usage) - .checked_mul(runtime_config.storage_amount_per_byte) - .ok_or_else(|| { - StorageError::StorageInconsistentState(format!( - "Account's storage_usage {} overflows multiplication", - account.storage_usage - )) - })?; - let available_amount = account.amount.checked_add(account.locked).ok_or_else(|| { - StorageError::StorageInconsistentState(format!( - "Account's amount {} and locked {} overflow addition", - account.amount, account.locked - )) - })?; - if available_amount >= required_amount { - Ok(None) - } else { - Ok(Some(required_amount - available_amount)) - } -} - pub(crate) fn get_code_with_cache( state_update: &TrieUpdate, account_id: &AccountId, diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 0c9a9482efe..1217845fcf3 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -20,6 +20,7 @@ use near_primitives::types::{ Nonce, RawStateChangesWithTrieKey, ShardId, StateChangeCause, StateRoot, ValidatorStake, }; use near_primitives::utils::{create_nonce_with_nonce, system_account}; +use near_runtime_configs::get_insufficient_storage_stake; use near_store::{ get, get_account, get_postponed_receipt, get_received_data, remove_postponed_receipt, set, set_access_key, set_account, set_code, set_postponed_receipt, set_received_data, @@ -500,7 +501,9 @@ impl Runtime { // Going to check balance covers account's storage. if result.result.is_ok() { if let Some(ref mut account) = account { - if let Some(amount) = get_insufficient_storage_stake(account, &self.config)? { + if let Some(amount) = get_insufficient_storage_stake(account, &self.config) + .map_err(|err| StorageError::StorageInconsistentState(err))? + { result.merge(ActionResult { result: Err(ActionError { index: None, diff --git a/runtime/runtime/src/verifier.rs b/runtime/runtime/src/verifier.rs index eef050af99b..75b2c2abaaf 100644 --- a/runtime/runtime/src/verifier.rs +++ b/runtime/runtime/src/verifier.rs @@ -1,6 +1,3 @@ -use crate::actions::get_insufficient_storage_stake; -use crate::config::{total_prepaid_gas, tx_cost, RuntimeConfig, TransactionCost}; -use crate::VerificationResult; use near_crypto::key_conversion::is_valid_staking_key; use near_primitives::account::AccessKeyPermission; use near_primitives::errors::{ @@ -13,10 +10,16 @@ use near_primitives::transaction::{ SignedTransaction, StakeAction, }; use near_primitives::utils::is_valid_account_id; -use near_store::{get_access_key, get_account, set_access_key, set_account, TrieUpdate}; +use near_runtime_configs::get_insufficient_storage_stake; +use near_store::{ + get_access_key, get_account, set_access_key, set_account, StorageError, TrieUpdate, +}; use near_vm_logic::types::Balance; use near_vm_logic::VMLimitConfig; +use crate::config::{total_prepaid_gas, tx_cost, RuntimeConfig, TransactionCost}; +use crate::VerificationResult; + /// Validates the transaction without using the state. It allows any node to validate a /// transaction before forwarding it to the node that tracks the `signer_id` account. pub fn validate_transaction( @@ -128,7 +131,9 @@ pub fn verify_and_charge_transaction( } .into()) } - Err(err) => return Err(RuntimeError::StorageError(err)), + Err(err) => { + return Err(RuntimeError::StorageError(StorageError::StorageInconsistentState(err))) + } }; if let AccessKeyPermission::FunctionCall(ref function_call_permission) = access_key.permission {