diff --git a/Cargo.lock b/Cargo.lock index d1b2afb95..b3db02560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.0.2" @@ -23,12 +38,101 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a1de45611fdb535bfde7b7de4fd54f4fd2b17b1737c0a59b69bf9b92074b8c" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bitflags 1.3.2", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdca6a10ecad987bda04e95606ef85a5417dcaac1a78455242d72e031e2b6b62" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.1" @@ -129,12 +233,27 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + [[package]] name = "bzip2-sys" version = "0.1.11+1.0.8" @@ -224,6 +343,15 @@ dependencies = [ "void", ] +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -267,6 +395,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -299,6 +447,7 @@ name = "electrs" version = "0.10.0" dependencies = [ "anyhow", + "axum", "bitcoin", "bitcoincore-rpc", "configure_me", @@ -318,6 +467,8 @@ dependencies = [ "signal-hook", "tempfile", "tiny_http", + "tokio", + "tower-http", ] [[package]] @@ -397,6 +548,58 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -408,12 +611,49 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64", + "bitflags 1.3.2", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.2" @@ -432,6 +672,40 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + [[package]] name = "httpdate" version = "1.0.2" @@ -444,6 +718,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -559,6 +856,12 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +[[package]] +name = "matchit" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" + [[package]] name = "memchr" version = "2.5.0" @@ -574,12 +877,38 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "nom" version = "7.1.3" @@ -600,6 +929,21 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + [[package]] name = "parking_lot" version = "0.12.1" @@ -635,6 +979,44 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.27" @@ -811,6 +1193,12 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -844,6 +1232,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -908,6 +1302,39 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.1.0" @@ -939,6 +1366,16 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "syn" version = "1.0.109" @@ -961,6 +1398,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "tempfile" version = "3.7.0" @@ -1015,6 +1458,37 @@ dependencies = [ "log", ] +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "toml" version = "0.5.11" @@ -1024,6 +1498,85 @@ dependencies = [ "serde", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" +dependencies = [ + "bitflags 2.3.3", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-ident" version = "1.0.11" @@ -1042,12 +1595,27 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index aaf2a4c7a..546a904c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ build = "build.rs" default = ["metrics"] metrics = ["prometheus", "tiny_http"] metrics_process = ["prometheus/process"] +http = ["axum", "tokio", "tower-http"] [package.metadata.configure_me] spec = "internal/config_specification.toml" [dependencies] anyhow = "1.0" +axum = { version = "0.6.17", features = ["headers", "macros"], optional = true } bitcoin = { version = "0.30.0", features = ["serde", "rand-std"] } bitcoincore-rpc = "0.17.0" configure_me = "0.4" @@ -36,6 +38,8 @@ serde = "1.0" serde_derive = "1.0" serde_json = "1.0" signal-hook = "0.3" +tokio = { version = "1.28.2", features = ["full"], optional = true } +tower-http = { version = "0.4.0", features = ["cors"], optional = true } tiny_http = { version = "0.12", optional = true } [dependencies.electrs-rocksdb] diff --git a/internal/config_specification.toml b/internal/config_specification.toml index 193dfc824..ec030a155 100644 --- a/internal/config_specification.toml +++ b/internal/config_specification.toml @@ -61,10 +61,16 @@ name = "electrum_rpc_addr" type = "crate::config::ResolvAddr" doc = "Electrum server JSONRPC 'addr:port' to listen on (default: '127.0.0.1:50001' for mainnet, '127.0.0.1:60001' for testnet, '127.0.0.1:60401' for regtest and '127.0.0.1:60601' for signet)" +[[param]] +name = "http_addr" +type = "crate::config::ResolvAddr" +doc = "Electrum server HTTP 'addr:port' to listen on (default: 127.0.0.1:3000 for mainnet, 127.0.0.1:3001 for testnet, 127.0.0.1:3002 for regtest and 127.0.0.1:3003 for signet)" + [[param]] name = "daemon_rpc_addr" type = "crate::config::ResolvAddr" doc = "Bitcoin daemon JSONRPC 'addr:port' to connect (default: 127.0.0.1:8332 for mainnet, 127.0.0.1:18332 for testnet, 127.0.0.1:18443 for regtest and 127.0.0.1:18554 for signet)" + [[param]] name = "daemon_p2p_addr" type = "crate::config::ResolvAddr" diff --git a/src/config.rs b/src/config.rs index 6d2395cbd..5d35ac52f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -131,6 +131,8 @@ pub struct Config { pub daemon_rpc_addr: SocketAddr, pub daemon_p2p_addr: SocketAddr, pub electrum_rpc_addr: SocketAddr, + #[cfg(feature = "http")] + pub http_addr: SocketAddr, pub monitoring_addr: SocketAddr, pub wait_duration: Duration, pub jsonrpc_timeout: Duration, @@ -233,6 +235,14 @@ impl Config { Network::Signet => 60601, unsupported => unsupported_network(unsupported), }; + #[cfg(feature = "http")] + let default_http_port = match config.network { + Network::Bitcoin => 3000, + Network::Testnet => 3001, + Network::Regtest => 3002, + Network::Signet => 3003, + unsupported => unsupported_network(unsupported), + }; let default_monitoring_port = match config.network { Network::Bitcoin => 4224, Network::Testnet => 14224, @@ -268,6 +278,11 @@ impl Config { (DEFAULT_SERVER_ADDRESS, default_electrum_port).into(), ResolvAddr::resolve_or_exit, ); + #[cfg(feature = "http")] + let http_addr: SocketAddr = config.http_addr.map_or( + (DEFAULT_SERVER_ADDRESS, default_http_port).into(), + ResolvAddr::resolve_or_exit, + ); #[cfg(not(feature = "metrics"))] { if config.monitoring_addr.is_some() { @@ -338,6 +353,8 @@ impl Config { daemon_rpc_addr, daemon_p2p_addr, electrum_rpc_addr, + #[cfg(feature = "http")] + http_addr, monitoring_addr, wait_duration: Duration::from_secs(config.wait_duration_secs), jsonrpc_timeout: Duration::from_secs(config.jsonrpc_timeout_secs), diff --git a/src/lib.rs b/src/lib.rs index d1df9c062..5134b3fae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ mod mempool; mod merkle; mod metrics; mod p2p; +#[cfg(feature = "http")] +mod rest; mod server; mod signals; mod status; diff --git a/src/rest.rs b/src/rest.rs new file mode 100644 index 000000000..dde0c74de --- /dev/null +++ b/src/rest.rs @@ -0,0 +1,418 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use anyhow::Result; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Router, +}; +use crossbeam_channel::{Receiver, Sender}; +use serde_json::Value; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + join, + net::TcpStream, + task, +}; +use tower_http::cors::CorsLayer; + +use crate::config::Config; + +// Transactions +async fn get_tx() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_status() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_hex() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_raw() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_merkleblock_proof() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_merkle_proof() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_outspend_and_vout() -> Result { + Ok(StatusCode::OK) +} + +async fn get_tx_outspends() -> Result { + Ok(StatusCode::OK) +} + +async fn post_tx() -> Result { + Ok(StatusCode::OK) +} + +// Addresses +async fn get_address() -> Result { + Ok(StatusCode::OK) +} + +async fn get_scripthash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_address_txs() -> Result { + Ok(StatusCode::OK) +} + +async fn get_scripthash_txs() -> Result { + Ok(StatusCode::OK) +} + +async fn get_confirmed_txs_by_address() -> Result { + Ok(StatusCode::OK) +} + +async fn get_last_seen_txs_by_address() -> Result { + Ok(StatusCode::OK) +} + +async fn get_confirmed_txs_by_scripthash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_last_seen_txs_by_scripthash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_unconfirmed_txs_by_address() -> Result { + Ok(StatusCode::OK) +} + +async fn get_unconfirmed_txs_by_scripthash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_utxos_by_address() -> Result { + Ok(StatusCode::OK) +} + +async fn get_utxos_by_scripthash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_addresses_by_prefix() -> Result { + Ok(StatusCode::OK) +} + +// Blocks +async fn get_block() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_header() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_status() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_txs() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_txs_by_index() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_txids() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_tx() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_raw() -> Result { + Ok(StatusCode::OK) +} + +async fn get_block_height_hash() -> Result { + Ok(StatusCode::OK) +} + +async fn get_blocks() -> Result { + Ok(StatusCode::OK) +} + +async fn get_blocks_by_height() -> Result { + Ok(StatusCode::OK) +} + +#[axum::debug_handler] +async fn get_block_height( + State(state): State>, +) -> Result { + let req = rpc_request(state.clone(), "blockchain.headers.subscribe", vec![]).await?; + let res: BlockchainHeadersSubscribe = serde_json::from_slice(&req)?; + + Ok((StatusCode::OK, res.block_height.to_string())) +} + +async fn get_block_tip_hash() -> Result { + Ok(StatusCode::OK) +} + +// Mempool +async fn get_mempool() -> Result { + Ok(StatusCode::OK) +} + +async fn get_mempool_txids() -> Result { + Ok(StatusCode::OK) +} + +async fn get_mempool_recent() -> Result { + Ok(StatusCode::OK) +} + +// Fee estimates +async fn get_fee_estimates() -> Result { + Ok(StatusCode::OK) +} + +#[allow(dead_code)] +#[derive(Deserialize)] +struct BlockchainHeadersSubscribe { + nonce: u64, + prev_block_hash: String, + timestamp: u64, + merkle_root: String, + block_height: u32, + utxo_root: String, + version: u32, + bits: u64, +} + +#[derive(Serialize)] +struct Request { + id: Value, + method: String, + params: Value, +} + +#[derive(Deserialize)] +struct RequestId { + id: usize, +} + +async fn rpc_request(state: Arc, method: &str, params: Vec) -> Result> { + let req_id = state.id.fetch_add(1, Ordering::SeqCst); + + let request = Request { + id: Value::from(req_id), + method: method.to_owned(), + params: Value::from(params), + }; + + let req = serde_json::to_vec(&request)?; + + state.req_tx.send(req)?; + debug!("hello C"); + + loop { + let (res_id, res) = state.res_rx.recv()?; + + debug!("hello D"); + + if req_id == res_id { + return Ok(res); + } + } +} + +struct AppState { + id: AtomicUsize, + req_tx: Sender>, + res_rx: Receiver<(usize, Vec)>, +} + +/// Esplora HTTP API according to these docs: +/// +pub async fn serve(config: Arc) -> Result<()> { + let socket = TcpStream::connect(config.daemon_rpc_addr).await?; + let (mut tcp_rx, mut tcp_tx) = socket.into_split(); + let id = AtomicUsize::new(0); + + let (req_tx, req_rx) = crossbeam_channel::unbounded::>(); + let (res_tx, res_rx) = crossbeam_channel::unbounded::<(usize, Vec)>(); + + let state = Arc::new(AppState { id, req_tx, res_rx }); + let headers_sub_state = state.clone(); + + // TCP writer + let tcp_writer = task::spawn(async move { + debug!("writer 1"); + loop { + let req = req_rx.recv().unwrap(); + debug!("HTTP Request: {}", String::from_utf8_lossy(&req)); + tcp_tx.write_all(&req).await.unwrap(); + debug!("writer 3"); + } + }); + + // TCP reader + let tcp_reader = task::spawn(async move { + let mut buf = [0; 1500]; + let mut brackets = 0; + let mut msg: Vec = vec![]; + + debug!("reader 1"); + while let Ok(_bytes) = tcp_rx.read(&mut buf).await { + debug!("reader 2"); + for byte in buf { + if byte == b'{' { + debug!("reader {{"); + brackets += 1; + } else if byte == b'}' { + debug!("reader }}"); + if brackets == 1 { + match serde_json::from_slice(&msg) { + Ok(res) => { + let RequestId { id } = res; + res_tx.send((id, msg)).unwrap(); + msg = vec![]; + } + Err(e) => { + error!("Error parsing message: {e}"); + debug!("Message was: {}", String::from_utf8_lossy(&msg)); + } + } + } + brackets -= 1; + } + msg.push(byte); + } + } + + debug!("reader msg: {}", String::from_utf8_lossy(&msg)); + }); + + let subscriber = task::spawn(async move { + rpc_request(headers_sub_state, "blockchain.headers.subscribe", vec![]) + .await + .unwrap(); + }); + + let server = task::spawn(async move { + let app = Router::new() + // Transactions + .route("/tx/:txid", get(get_tx)) + .route("/tx/:txid/status", get(get_tx_status)) + .route("/tx/:txid/hex", get(get_tx_hex)) + .route("/tx/:txid/raw", get(get_tx_raw)) + .route("/tx/:txid/merkleblock-proof", get(get_tx_merkleblock_proof)) + .route("/tx/:txid/merkle-proof", get(get_tx_merkle_proof)) + .route("/tx/:txid/outspend/:vout", get(get_tx_outspend_and_vout)) + .route("/tx/:txid/outspends", get(get_tx_outspends)) + .route("/tx", post(post_tx)) + // Addresses + .route("/address/:address", get(get_address)) + .route("/scripthash/:hash", get(get_scripthash)) + .route("/address/:address/txs", get(get_address_txs)) + .route("/scripthash/:hash/txs", get(get_scripthash_txs)) + .route( + "/address/:address/txs/chain", + get(get_confirmed_txs_by_address), + ) + .route( + "/address/:address/txs/chain/:last_seen_txid", + get(get_last_seen_txs_by_address), + ) + .route( + "/scripthash/:hash/txs/chain", + get(get_confirmed_txs_by_scripthash), + ) + .route( + "/scripthash/:hash/txs/chain/:last_seen_txid", + get(get_last_seen_txs_by_scripthash), + ) + .route( + "/address/:address/txs/mempool", + get(get_unconfirmed_txs_by_address), + ) + .route( + "/scripthash/:hash/txs/mempool", + get(get_unconfirmed_txs_by_scripthash), + ) + .route("/address/:address/utxo", get(get_utxos_by_address)) + .route("/scripthash/:hash/utxo", get(get_utxos_by_scripthash)) + .route("/address-prefix/:prefix", get(get_addresses_by_prefix)) + // Blocks + .route("/block/:hash", get(get_block)) + .route("/block/:hash/header", get(get_block_header)) + .route("/block/:hash/status", get(get_block_status)) + .route("/block/:hash/txs", get(get_block_txs)) + .route("/block/:hash/txs/:start_index", get(get_block_txs_by_index)) + .route("/block/:hash/txids", get(get_block_txids)) + .route("/block/:hash/txid/:index", get(get_block_tx)) + .route("/block/:hash/raw", get(get_block_raw)) + .route("/block-height/:height", get(get_block_height_hash)) + .route("/blocks", get(get_blocks)) + .route("/blocks/:start_height", get(get_blocks_by_height)) + .route("/blocks/tip/height", get(get_block_height)) + .route("/blocks/tip/hash", get(get_block_tip_hash)) + // Mempool + .route("/mempool", get(get_mempool)) + .route("/mempool/txids", get(get_mempool_txids)) + .route("/mempool/recent", get(get_mempool_recent)) + // Fee estimates + .route("/fee-estimates", get(get_fee_estimates)) + .with_state(state) + .layer(CorsLayer::permissive()); + + info!("serving Esplora REST on {}", config.http_addr); + + axum::Server::bind(&config.http_addr) + .serve(app.into_make_service()) + .await + .unwrap(); + }); + + let _ = join!(tcp_writer, tcp_reader, subscriber, server); + + Ok(()) +} + +struct AppError(anyhow::Error); + +// Tell axum how to convert `AppError` into a response. +impl IntoResponse for AppError { + fn into_response(self) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Something went wrong: {}", self.0), + ) + .into_response() + } +} + +// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into +// `Result<_, AppError>`. That way you don't need to do that manually. +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} diff --git a/src/server.rs b/src/server.rs index f50662f70..da118f4bc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,12 +1,15 @@ use anyhow::{Context, Result}; use crossbeam_channel::{select, unbounded, Sender}; use rayon::prelude::*; +#[cfg(feature = "http")] +use tokio::runtime::Runtime; use std::{ collections::hash_map::HashMap, io::{BufRead, BufReader, Write}, iter::once, net::{Shutdown, TcpListener, TcpStream}, + sync::Arc, }; use crate::{ @@ -17,6 +20,9 @@ use crate::{ thread::spawn, }; +#[cfg(feature = "http")] +use crate::rest; + struct Peer { id: usize, client: Client, @@ -61,7 +67,7 @@ pub fn run() -> Result<()> { } fn serve() -> Result<()> { - let config = Config::from_args(); + let config = Arc::new(Config::from_args()); let metrics = Metrics::new(config.monitoring_addr)?; let (server_tx, server_rx) = unbounded(); @@ -69,6 +75,32 @@ fn serve() -> Result<()> { let listener = TcpListener::bind(config.electrum_rpc_addr)?; info!("serving Electrum RPC on {}", listener.local_addr()?); spawn("accept_loop", || accept_loop(listener, server_tx)); // detach accepting thread + + #[cfg(feature = "http")] + let config = config.clone(); + #[cfg(feature = "http")] + { + use crossbeam_channel::bounded; + + let (http_status_tx, http_status_rx) = bounded::>(1); + + let rt = Runtime::new()?; + rt.spawn(async move { + match rest::serve(config.clone()).await { + Ok(_) => { + http_status_tx.send(None).unwrap(); + } + Err(e) => { + error!("error in http server: {}", e); + http_status_tx.send(Some(e.to_string())).unwrap(); + } + } + }); + + if let Some(err) = http_status_rx.recv().unwrap() { + return Err(anyhow!(err)); + } + } }; let server_batch_size = metrics.histogram_vec( @@ -87,6 +119,7 @@ fn serve() -> Result<()> { let new_block_rx = rpc.new_block_notification(); let mut peers = HashMap::::new(); + loop { // initial sync and compaction may take a few hours while server_rx.is_empty() {