diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 0866843..3af29a9 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,8 +38,8 @@ jobs: - name: Run Clippy linter run: make lint - # - name: Run Clippy linter for wasm target - # run: make lint-wasm + - name: Run Clippy linter for wasm target + run: make lint-wasm - name: Check code formatting run: make fmt-check diff --git a/Cargo.lock b/Cargo.lock index 9017034..5d2bd1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -43,36 +43,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -175,25 +175,26 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "casper-binary-port" version = "1.0.0" -source = "git+https://github.com/casper-network/casper-node.git?branch=feat-2.0#79714924f7a83d98e0d3037fef60fcb66ce4ef54" +source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#91fbb870e76760980c8025e1ab9b552532444e1e" dependencies = [ "bincode", "bytes", "casper-types", "once_cell", "rand", - "schemars", "serde", - "serde-map-to-array", + "strum", + "strum_macros", "thiserror", "tokio-util", + "tracing", ] [[package]] @@ -203,9 +204,14 @@ dependencies = [ "casper-binary-port", "casper-types", "futures", + "gloo-utils", + "js-sys", + "rand", "thiserror", "tokio", - "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -227,7 +233,7 @@ dependencies = [ [[package]] name = "casper-types" version = "5.0.0" -source = "git+https://github.com/casper-network/casper-node.git?branch=feat-2.0#79714924f7a83d98e0d3037fef60fcb66ce4ef54" +source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#91fbb870e76760980c8025e1ab9b552532444e1e" dependencies = [ "base16", "base64", @@ -271,9 +277,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -281,9 +287,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -301,20 +307,20 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "const-oid" @@ -330,9 +336,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -399,7 +405,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -442,7 +448,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -560,12 +566,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -640,7 +646,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -703,6 +709,19 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "group" version = "0.13.0" @@ -722,9 +741,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -732,12 +751,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -778,12 +791,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", ] [[package]] @@ -803,16 +816,17 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -830,9 +844,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linux-raw-sys" @@ -854,20 +868,19 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", @@ -914,7 +927,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -960,9 +973,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -992,9 +1005,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1023,18 +1036,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1071,9 +1084,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1083,9 +1096,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1125,17 +1138,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -1164,7 +1183,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -1182,15 +1201,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "serde" -version = "1.0.211" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac55e59090389fb9f0dd9e0f3c09615afed1d19094284d0b200441f13550793" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -1216,13 +1235,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.211" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be4f245ce16bc58d57ef2716271d0d4519e0f6defa147f6e081005bcb278ff" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -1233,16 +1252,16 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.0", "itoa", "memchr", "ryu", @@ -1281,9 +1300,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1311,6 +1330,25 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.94", +] + [[package]] name = "subtle" version = "2.6.1" @@ -1330,9 +1368,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" dependencies = [ "proc-macro2", "quote", @@ -1341,9 +1379,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -1351,31 +1389,32 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] name = "tokio" -version = "1.41.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", + "bytes", "libc", "mio", "pin-project-lite", @@ -1392,7 +1431,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] @@ -1411,19 +1450,34 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.94", +] + [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] [[package]] name = "typeid" @@ -1451,9 +1505,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "untrusted" @@ -1481,9 +1535,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -1492,24 +1546,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1517,22 +1583,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "web-sys" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "windows-sys" @@ -1634,7 +1710,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.82", + "syn 2.0.94", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c8d8900..c19965b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["binary_port_access"] +members = ["binary-port-access"] [package] name = "casper-binary-port-client" @@ -11,15 +11,15 @@ description = "CLI for Casper binary port." license = "Apache-2.0" [dependencies] -casper-types = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0", features = [ +casper-types = { git = "https://github.com/casper-network/casper-node", branch = "feat-2.0", features = [ "std-fs-io", ] } -casper-binary-port = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } -casper-binary-port-access = { path = "./binary_port_access" } -clap = { version = "4.5.3", features = ["derive", "wrap_help"] } -thiserror = "1.0.58" -tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread", "net"] } +casper-binary-port = { version = "1.0.0", git = "https://github.com/casper-network/casper-node", branch = "feat-2.0" } +casper-binary-port-access = { path = "./binary-port-access" } +clap = { version = "4.5.20", features = ["derive", "wrap_help"] } +thiserror = "1.0.64" +tokio = { version = "1.41.0", features = ["macros", "rt", "net"] } hex = "0.4.3" -serde = { version = "1.0.203", features = ["derive"] } -serde_json = "1.0.117" +serde = { version = "1.0.211", features = ["derive"] } +serde_json = "1.0.132" erased-serde = "0.4.5" diff --git a/Makefile b/Makefile index ad24a1c..7a4be93 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ lint: # Run clippy on wasm32-unknown-unknown target lint-wasm: - cd binary_port_access && cargo clippy --target $(WASM_TARGET) --all-targets -- -D warnings + cd binary-port-access && cargo clippy --target $(WASM_TARGET) --all-targets -- -D warnings # Format the codebase using rustfmt and check for correct formatting fmt-check: @@ -31,7 +31,7 @@ outdated: cargo outdated || cargo install cargo-outdated # Run a full CI-style check (build, fmt, clippy, test) -ci-check: fmt lint lint-wasm test +ci-check: fmt-check lint lint-wasm test # Clean build artifacts clean: diff --git a/README.md b/README.md index 7bec3c4..2d21925 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,10 @@ Commands: Options: -v, --verbose Provides a verbose output as the command is being handled (not supported yet) - -n, --node-address + -n, --node-address -h, --help Print help ``` + To get further info on any command, run `help` followed by the subcommand, e.g. @@ -46,15 +47,16 @@ Retrieve block header by height or hash Usage: casper-binary-port-client information block-header [OPTIONS] Options: - --hash - --height + --hash + --height -h, --help Print help ``` + ## Client library -The `binary_port_access` directory contains source for the client library, which may be called directly rather than through the CLI binary. The CLI app `casper-binary-port-client` makes use of this library to implement its functionality. +The `binary-port-access` directory contains source for the client library, which may be called directly rather than through the CLI binary. The CLI app `casper-binary-port-client` makes use of this library to implement its functionality. ## License diff --git a/binary-port-access/Cargo.toml b/binary-port-access/Cargo.toml new file mode 100644 index 0000000..77ed9ad --- /dev/null +++ b/binary-port-access/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "casper-binary-port-access" +version = "0.1.0" +edition = "2021" +authors = ["Rafał Chabowski "] +description = "Library for accessing Casper binary port." +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib", "rlib"] +name = "casper_binary_port_access" +path = "src/lib.rs" + +[dependencies] +casper-types = { git = "https://github.com/casper-network/casper-node", default-features = false, branch = "feat-2.0" } +casper-binary-port = { git = "https://github.com/casper-network/casper-node", branch = "feat-2.0" } +thiserror = "1.0.64" +futures = "0.3.31" +rand = { version = "0.8.5", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "*" +wasm-bindgen-futures = "*" +js-sys = "*" +web-sys = { version = "0.3", features = [ + "Blob", + "FileReader", + "ProgressEvent", + "WebSocket", + "MessageEvent", +] } +gloo-utils = { version = "0.2", default-features = false, features = ["serde"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tokio = { version = "1.40.0", features = ["macros", "io-util", "time", "net"] } diff --git a/binary-port-access/src/communication/common.rs b/binary-port-access/src/communication/common.rs new file mode 100644 index 0000000..c9b004f --- /dev/null +++ b/binary-port-access/src/communication/common.rs @@ -0,0 +1,373 @@ +use crate::Error; +#[cfg(not(target_arch = "wasm32"))] +use casper_binary_port::BinaryMessage; +use casper_binary_port::{ + BinaryRequest, BinaryRequestHeader, BinaryResponse, BinaryResponseAndRequest, PayloadEntity, +}; +use casper_types::{ + bytesrepr::{self, FromBytes, ToBytes}, + ProtocolVersion, +}; +#[cfg(not(target_arch = "wasm32"))] +use rand::Rng; +#[cfg(not(target_arch = "wasm32"))] +use std::time::Duration; +#[cfg(not(target_arch = "wasm32"))] +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + time::timeout, +}; + +// TODO[RC]: Do not hardcode this. +pub(crate) const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(2, 0, 0); +pub(crate) const LENGTH_FIELD_SIZE: usize = 4; +#[cfg(not(target_arch = "wasm32"))] +const TIMEOUT_DURATION: Duration = Duration::from_secs(5); + +/// Establishes an asynchronous TCP connection to a specified node address. +/// +/// This function attempts to connect to a node using a TCP stream. It is only +/// compiled for non-WebAssembly (Wasm) targets, making it suitable for native +/// applications. +/// +/// # Parameters +/// +/// - `node_address`: A `&str` representing the address of the node to which +/// the connection will be made. This should include the host and port (e.g., +/// "localhost:28101"). The default port to use is `28101`. +/// +/// # Returns +/// +/// This function returns a `Result` that, on success, contains a `TcpStream` +/// which represents the established connection. On failure, it returns a +/// `std::io::Error`. +/// +/// # Errors +/// +/// This function may return an error if: +/// - The connection to the specified `node_address` fails. +/// - There are network issues preventing the establishment of the connection. +/// - The address format is invalid. +/// +/// # Notes +/// +/// This function is only compiled for targets other than `wasm32`, ensuring it +/// is used in appropriate environments such as servers or local applications. +#[cfg(not(target_arch = "wasm32"))] +async fn connect_to_node(node_address: &str) -> Result { + let stream = TcpStream::connect(node_address).await?; + Ok(stream) +} + +/// Sends the payload length and data to the connected TCP client. +/// +/// This asynchronous function sends a binary message to a TCP client. It first sends +/// the length of the message payload as a 4-byte little-endian integer for Casper binary protocol, followed by +/// the actual payload data. This function is intended for use in non-WebAssembly (Wasm) +/// environments, typically on servers or local applications. +/// +/// # Parameters +/// +/// - `client`: A mutable reference to a `TcpStream` representing the connection +/// to the client to which the payload will be sent. +/// - `message`: A reference to a `BinaryMessage` containing the payload data to send. +/// +/// # Returns +/// +/// This function returns a `Result` indicating the outcome of the operation: +/// - `Ok(())`: Indicates the payload was sent successfully. +/// - `Err(Error)`: An error if any part of the send operation fails, including timeout errors. +/// +/// # Errors +/// +/// This function may return an error if: +/// - The write operations timeout, resulting in a `TimeoutError`. +/// - There are issues with the TCP stream that prevent data from being sent. +/// +/// # Notes +/// +/// The function ensures that the TCP connection remains responsive by enforcing a timeout +/// on each write operation. This prevents the function from hanging indefinitely in case +/// of network issues or an unresponsive client. The payload length is sent first, allowing +/// the client to know how many bytes to expect for the subsequent payload data. +#[cfg(not(target_arch = "wasm32"))] +async fn send_payload(client: &mut TcpStream, message: &BinaryMessage) -> Result<(), Error> { + let payload_length = message.payload().len() as u32; + let length_bytes = payload_length.to_le_bytes(); + let _ = timeout(TIMEOUT_DURATION, client.write_all(&length_bytes)) + .await + .map_err(|e| Error::TimeoutError(e.to_string()))?; + + let _ = timeout(TIMEOUT_DURATION, client.write_all(message.payload())) + .await + .map_err(|e| Error::TimeoutError(e.to_string()))?; + + let _ = timeout(TIMEOUT_DURATION, client.flush()) + .await + .map_err(|e| Error::TimeoutError(e.to_string()))?; + Ok(()) +} + +/// Reads the response from a connected TCP client and returns the response buffer. +/// +/// This asynchronous function reads a response from a TCP stream. It first reads +/// a length prefix to determine how many bytes to read for the actual response. +/// The function is only available for non-WebAssembly (Wasm) targets, ensuring +/// it is used in appropriate environments such as servers or local applications. +/// +/// # Parameters +/// +/// - `client`: A mutable reference to a `TcpStream` representing the connection +/// to the client from which the response will be read. +/// +/// # Returns +/// +/// This function returns a `Result` containing: +/// - `Ok(Vec)`: A vector of bytes representing the response data from the client. +/// - `Err(Error)`: An error if the read operation fails, including timeout errors. +/// +/// # Errors +/// +/// This function may return an error if: +/// - The read operation times out, resulting in a `TimeoutError`. +/// - There are issues reading from the TCP stream, which may yield an `Error`. +/// +/// # Notes +/// +/// The first 4 bytes read from the stream are interpreted as a little-endian +/// unsigned integer indicating the length of the subsequent response data. +/// The function enforces a timeout for read operations to prevent hanging +/// indefinitely on slow or unresponsive clients. +#[cfg(not(target_arch = "wasm32"))] +async fn read_response(client: &mut TcpStream) -> Result, Error> { + let mut length_buf = [0u8; LENGTH_FIELD_SIZE]; + let _ = timeout(TIMEOUT_DURATION, client.read_exact(&mut length_buf)) + .await + .map_err(|e| Error::TimeoutError(e.to_string()))?; + + let response_length = u32::from_le_bytes(length_buf) as usize; + let mut response_buf = vec![0u8; response_length]; + let _ = timeout(TIMEOUT_DURATION, client.read_exact(&mut response_buf)) + .await + .map_err(|e| Error::TimeoutError(e.to_string()))?; + Ok(response_buf) +} + +/// Sends a binary request to a node and waits for the response. +/// +/// This asynchronous function establishes a TCP connection to the specified node address, +/// sends a serialized binary request, and processes the response. It generates a unique +/// request ID for each request to correlate with the response received. +/// +/// # Parameters +/// +/// - `node_address`: A string slice that holds the address of the node to connect to, +/// typically in the format "hostname:28101". +/// - `request`: An instance of `BinaryRequest` representing the request data to be sent. +/// +/// # Returns +/// +/// This function returns a `Result` that indicates the outcome of the operation: +/// - `Ok(BinaryResponseAndRequest)`: The processed response received from the node. +/// - `Err(Error)`: An error if any part of the operation fails, including connection issues, +/// serialization errors, or response processing errors. +/// +/// # Errors +/// +/// This function may return an error if: +/// - The connection to the node fails, returning an `Error::ConnectionError`. +/// - Serialization of the request fails, leading to an unwrapped panic in case of a serialization error. +/// - Sending the payload or reading the response times out, resulting in a `TimeoutError`. +/// +/// # Notes +/// +/// The function uses a unique request ID for each request, allowing the response to be +/// associated with the correct request. The payload is sent in two parts: first the length +/// of the payload as a 4-byte little-endian integer, and then the actual payload data. +/// After sending the request, it waits for the response and processes it accordingly. +/// This function is designed to be used in non-WebAssembly (Wasm) environments, typically +/// on servers or local applications. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn send_request( + node_address: &str, + request: BinaryRequest, +) -> Result { + let request_id = rand::thread_rng().gen::(); + let payload = + encode_request(&request, Some(request_id)).expect("should always serialize a request"); + let mut client = connect_to_node(node_address).await?; + + let message = BinaryMessage::new(payload); + + // Send the payload length and data + send_payload(&mut client, &message).await?; + + // Read and process the response + let response_buf = read_response(&mut client).await?; + // Now process the response using the request_id + process_response(response_buf, request_id).await +} + +/// Encodes a binary request into a byte vector for transmission. +/// +/// This function serializes a `BinaryRequest` along with a specified request ID (if provided) +/// into a byte vector. The encoded data includes a header containing the protocol version, +/// request tag, and the request ID. This byte vector can then be sent over a network connection. +/// +/// # Parameters +/// +/// - `req`: A reference to a `BinaryRequest` instance representing the request to be serialized. +/// - `request_id`: An optional `u16` representing the unique identifier for the request. If not provided, +/// a default value of `0` is used. +/// +/// # Returns +/// +/// This function returns a `Result` that indicates the outcome of the operation: +/// - `Ok(Vec)`: A vector of bytes representing the serialized request, including the header and payload. +/// - `Err(bytesrepr::Error)`: An error if the serialization process fails, indicating the nature of the issue. +/// +/// # Errors +/// +/// The function may return an error if: +/// - Writing the header or the request data to the byte vector fails, which could be due to various +/// reasons, such as insufficient memory or incorrect data structures. +/// +/// # Notes +/// +/// The request ID helps in tracking requests and their corresponding responses, allowing for easier +/// identification in asynchronous communication. +pub(crate) fn encode_request( + req: &BinaryRequest, + request_id: Option, +) -> Result, bytesrepr::Error> { + let header = BinaryRequestHeader::new( + SUPPORTED_PROTOCOL_VERSION, + req.tag(), + request_id.unwrap_or_default(), + ); + let mut bytes = Vec::with_capacity(header.serialized_length() + req.serialized_length()); + header.write_bytes(&mut bytes)?; + req.write_bytes(&mut bytes)?; + Ok(bytes) +} + +/// Parses a binary response and deserializes it into a specified type. +/// +/// This function inspects the `BinaryResponse` to determine the type of returned data. If the +/// data type matches the expected type (specified by the generic type parameter `A`), it +/// deserializes the payload into an instance of `A`. +/// +/// # Parameters +/// +/// - `response`: A reference to a `BinaryResponse` instance containing the data to be parsed. +/// +/// # Type Parameters +/// +/// - `A`: A type that implements both `FromBytes` and `PayloadEntity` traits, indicating +/// that the type can be deserialized from a byte slice and represents a valid payload entity. +/// +/// # Returns +/// +/// This function returns a `Result` indicating the outcome of the operation: +/// - `Ok(Some(A))`: If the response type matches, the payload is successfully deserialized +/// into an instance of `A`. +/// - `Ok(None)`: If no data type tag is found in the response, indicating an empty or +/// invalid response payload. +/// - `Err(Error)`: If the data type tag does not match the expected type or if deserialization +/// fails, an error is returned providing details about the issue. +/// +/// # Errors +/// +/// The function may return an error if: +/// - The returned data type tag does not match the expected type. +/// - Deserialization of the payload into type `A` fails due to an invalid byte format or +/// insufficient data. +/// +/// # Notes +/// +/// This function is useful in scenarios where responses from a binary protocol need to be +/// dynamically parsed into specific types based on the data type tag. The use of the +/// `FromBytes` trait allows for flexible and type-safe deserialization. +pub(crate) fn parse_response( + response: &BinaryResponse, +) -> Result, Error> { + match response.returned_data_type_tag() { + Some(found) if found == u8::from(A::RESPONSE_TYPE) => { + // Verbose: Print length of payload + let payload = response.payload(); + let _payload_length = payload.len(); + // TODO[GR] use tracing::info! instead of dbg! + // dbg!(_payload_length); + + Ok(Some(bytesrepr::deserialize_from_slice(payload)?)) + } + Some(other) => Err(Error::Response(format!( + "unsupported response type: {other}" + ))), + _ => Ok(None), + } +} + +/// Processes the response buffer and checks for a request ID mismatch. +/// +/// This function takes a response buffer, extracts the request ID from the beginning of the buffer, +/// and checks it against the expected request ID. If the IDs match, it proceeds to deserialize the +/// remaining data in the buffer into a `BinaryResponseAndRequest` object. +/// +/// # Parameters +/// +/// - `response_buf`: A vector of bytes representing the response data received from the server. +/// - `request_id`: The expected request ID that was sent with the original request. +/// +/// # Returns +/// +/// This function returns a `Result` indicating the outcome of the operation: +/// - `Ok(BinaryResponseAndRequest)`: If the request ID matches and the response data is successfully +/// deserialized, it returns the deserialized `BinaryResponseAndRequest`. +/// - `Err(Error)`: If there is a mismatch in the request ID or if deserialization fails, an error +/// is returned providing details about the issue. +/// +/// # Errors +/// +/// The function may return an error if: +/// - The extracted request ID does not match the expected request ID, indicating a potential issue +/// with request handling or communication. +/// - Deserialization of the response buffer into `BinaryResponseAndRequest` fails due to an invalid +/// byte format or insufficient data. +pub(crate) async fn process_response( + response_buf: Vec, + request_id: u16, +) -> Result { + const REQUEST_ID_START: usize = 0; + const REQUEST_ID_END: usize = REQUEST_ID_START + 2; + + // Check if the response buffer is at least the size of the request ID + if response_buf.len() < REQUEST_ID_END { + return Err(Error::Response(format!( + "Response buffer is too small: expected at least {} bytes, got {}. Buffer contents: {:?}", + REQUEST_ID_END, + response_buf.len(), + response_buf + ))); + } + + // Extract Request ID from the response + let _request_id = u16::from_le_bytes( + response_buf[REQUEST_ID_START..REQUEST_ID_END] + .try_into() + .expect("Failed to extract Request ID"), + ); + + // Check if request_id matches _request_id and return an error if not + if request_id != _request_id { + return Err(Error::Response(format!( + "Request ID mismatch: expected {}, got {}", + request_id, _request_id + ))); + } + + // Deserialize the remaining response data + let response: BinaryResponseAndRequest = bytesrepr::deserialize_from_slice(response_buf)?; + Ok(response) +} diff --git a/binary-port-access/src/communication/mod.rs b/binary-port-access/src/communication/mod.rs new file mode 100644 index 0000000..b95212e --- /dev/null +++ b/binary-port-access/src/communication/mod.rs @@ -0,0 +1,11 @@ +/// The `communication` module provides both shared and platform-specific functionalities +/// for managing communication processes. +/// +/// # Modules +/// - [`common`]: Contains utilities and shared logic that are used across all target architectures. +pub mod common; + +/// - [`wasm32`]: Contains WebAssembly-specific (`wasm32` target) communication utilities, +/// available only when compiled for WebAssembly. +#[cfg(target_arch = "wasm32")] +pub mod wasm32; diff --git a/binary-port-access/src/communication/wasm32/mod.rs b/binary-port-access/src/communication/wasm32/mod.rs new file mode 100644 index 0000000..5592fe2 --- /dev/null +++ b/binary-port-access/src/communication/wasm32/mod.rs @@ -0,0 +1,658 @@ +#[cfg(target_arch = "wasm32")] +use crate::communication::common::{encode_request, process_response, LENGTH_FIELD_SIZE}; +#[cfg(target_arch = "wasm32")] +use crate::Error; +#[cfg(target_arch = "wasm32")] +use casper_binary_port::{BinaryRequest, BinaryResponseAndRequest}; +#[cfg(target_arch = "wasm32")] +use gloo_utils::format::JsValueSerdeExt; +#[cfg(target_arch = "wasm32")] +use js_sys::{JsString, Promise, Reflect}; +#[cfg(target_arch = "wasm32")] +use node_tcp_helper::NODE_TCP_HELPER; +#[cfg(target_arch = "wasm32")] +use rand::Rng; +#[cfg(target_arch = "wasm32")] +use std::cell::RefCell; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::{prelude::Closure, prelude::*, JsCast, JsValue}; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen_futures::JsFuture; +#[cfg(target_arch = "wasm32")] +use web_sys::{MessageEvent, WebSocket}; + +#[cfg(target_arch = "wasm32")] +pub mod node_tcp_helper; + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_with_prefix(s: &str); +} +/// Logs an error message, prefixing it with "error wasm" and sends it to the console in JavaScript when running in a WebAssembly environment. +/// When running outside WebAssembly, it prints the error message to the standard output. +#[cfg(target_arch = "wasm32")] +fn log(s: &str) { + #[cfg(target_arch = "wasm32")] + let prefixed_s = format!("log wasm {}", s); + #[cfg(target_arch = "wasm32")] + log_with_prefix(&prefixed_s); + #[cfg(not(target_arch = "wasm32"))] + println!("{}", s); +} + +/// Determines if the current environment is Node.js. +/// +/// This function checks for the presence of the `process` global object and +/// verifies that it has a `versions` property, which is characteristic of +/// a Node.js environment. +/// +/// # Returns +/// +/// Returns `true` if the current environment is identified as Node.js, and +/// `false` otherwise. +/// +/// # Notes +/// +/// This function is compiled only when targeting the `wasm32` architecture, +/// ensuring that it is not included in builds for other targets. +#[cfg(target_arch = "wasm32")] +fn is_node() -> bool { + // Check if 'process' exists and if it has 'versions' property (which is present in Node.js) + js_sys::global() + .dyn_into::() + .map(|global| { + Reflect::has(&global, &JsString::from("process")).unwrap_or(false) + && Reflect::get(&global, &JsString::from("process")) + .map(|process| { + Reflect::has(&process, &JsString::from("versions")).unwrap_or(false) + }) + .unwrap_or(false) + }) + .unwrap_or(false) +} + +/// Opens a TCP connection to a specified binary server and sends a payload. +/// +/// This asynchronous function establishes a TCP connection to a binary +/// server. It sends a specified payload and +/// waits for a response. The connection is made using a JavaScript script +/// executed in the WebAssembly context, leveraging Node.js's `net` module. +/// +/// # Parameters +/// +/// - `node_address`: A string that specifies the address of server +/// in the format "host:port". Typically "127.0.0.1:28101" +/// - `payload`: A `Vec` containing the data to be sent to the server. +/// - `request_id`: A unique identifier for the request, used to process +/// the response appropriately. +/// +/// # Returns +/// +/// This function returns a `Result` containing either a `BinaryResponseAndRequest` +/// on success or an `Error` on failure. +/// +/// # Errors +/// +/// This function may return an `Error` if: +/// - The JavaScript execution fails. +/// - The connection to the TCP server cannot be established. +/// - There is an error in sending or receiving data. +/// +/// # Notes +/// +/// This function is only compiled for the `wasm32` target, ensuring that +/// it does not affect builds for other architectures. +#[cfg(target_arch = "wasm32")] +async fn open_tcp_connection( + node_address: &str, + payload: Vec, + request_id: u16, +) -> Result { + use node_tcp_helper::sanitize_input; + + let parts: Vec<&str> = node_address.split(':').collect(); + let host = *parts + .first() + .ok_or_else(|| Error::Response("Missing host".to_string()))?; + let port = *parts + .last() + .ok_or_else(|| Error::Response("Missing port".to_string()))?; + + // Prepare the payload buffer + let buffer_payload = &format!("{:?}", payload); + + let sanitized_buffer_payload = sanitize_input(buffer_payload); + let sanitized_host = sanitize_input(host); + let sanitized_port = sanitize_input(port); + + let tcp_script = NODE_TCP_HELPER + .replace("{buffer_payload}", &sanitized_buffer_payload) + .replace("{host}", &sanitized_host) + .replace("{port}", &sanitized_port); + + // Execute the script in JavaScript context using eval (tcp_script is local but it requires "require" Js module not available in a classic function js_sys::Function) + let tcp_promise: Promise = js_sys::eval(&tcp_script) + .map_err(|err| { + log("Failed to execute TCP script in eval"); + log(&err.as_string().unwrap()); + Error::Response("Failed to execute TCP script".to_string()) + })? + .dyn_into() + .map_err(|err| { + log("Failed to cast eval result to Promise"); + log(&err.as_string().unwrap()); + Error::Response("Failed to cast eval result to Promise".to_string()) + })?; + + let js_future = JsFuture::from(tcp_promise); + let tcp_response = js_future + .await + .map_err(|e| Error::Response(format!("TCP connection promise failed: {:?}", e)))?; + + // Since the resolved value is a Buffer, convert it to a byte slice + let response_bytes = js_sys::Uint8Array::new(&tcp_response).to_vec(); + + // Log the received response bytes for debugging + // log(&format!("Received response data: {:?}", response_bytes)); + + // Read and process the response + let response_buf = read_response(response_bytes).await?; + // Now process the response using the request_id + process_response(response_buf, request_id).await +} + +/// Handles a WebSocket connection, sending a payload and awaiting a response. +/// +/// This asynchronous function manages a WebSocket connection by sending a +/// specified payload to a server and waiting for a binary response. It first +/// sends the length of the payload, followed by the payload itself. The response +/// is received through a message event, processed, and returned. +/// +/// # Parameters +/// +/// - `web_socket`: An instance of `WebSocket` used to establish the connection +/// and communicate with the server. +/// - `payload`: A `Vec` containing the data to be sent over the WebSocket. +/// - `request_id`: A unique identifier for the request, used for processing the +/// response later. +/// +/// # Returns +/// +/// This function returns a `Result` that, on success, contains a +/// `BinaryResponseAndRequest`, and on failure, contains an `Error`. +/// +/// # Errors +/// +/// This function may return an `Error` if: +/// - The WebSocket connection fails to open. +/// - There is an error sending the length buffer or payload. +/// - The WebSocket encounters an error during communication. +/// - The received message cannot be processed correctly. +/// +/// # Notes +/// +/// This function is only compiled for the `wasm32` target, making it suitable +/// for WebAssembly applications where WebSocket communication is required. +#[cfg(target_arch = "wasm32")] +async fn handle_websocket_connection( + web_socket: &WebSocket, + payload: Vec, + request_id: u16, +) -> Result { + let payload_length = payload.len() as u32; + let length_buffer = create_length_buffer(payload_length); + // log("Payload length buffer prepared."); + + let length_js_value = js_sys::Uint8Array::from(length_buffer.as_slice()); + + // If socket is not opened + let promise: Promise = if web_socket.ready_state() != WebSocket::OPEN { + Promise::new(&mut |resolve, reject| { + let web_socket_clone = web_socket.clone(); // Clone for the closure + let length_js_value = length_js_value.clone(); // Clone for the closure + let payload_clone = payload.clone(); // Clone for the closure + + // Set up onopen handler to send length and then payload + let onopen = { + Closure::wrap(Box::new(move || { + send_length_and_payload( + &web_socket_clone, + &length_js_value, + &payload_clone, + &resolve, + &reject, + ); + }) as Box) + }; + + let web_socket_ref = web_socket.unchecked_ref::(); + web_socket_ref.set_onopen(Some(onopen.as_ref().unchecked_ref())); + onopen.forget(); // Prevent memory leak by forgetting the closure + }) + } + // Socket is already opened + else { + Promise::new(&mut |resolve, reject| { + let web_socket_clone = web_socket.clone(); // Clone for the closure + let length_js_value = length_js_value.clone(); // Clone for the closure + let payload_clone = payload.clone(); // Clone for the closure + + // Directly send length and payload + send_length_and_payload( + &web_socket_clone, + &length_js_value, + &payload_clone, + &resolve, + &reject, + ); + }) + }; + + let js_future = JsFuture::from(js_sys::Promise::resolve(&promise)); + + // Await the resolved value from the promise + let onmessage = js_future.await.map_err(|e| { + log(&format!("Failed to receive message: {:?}", e)); + Error::Response(format!("Failed to receive message: {:?}", e)) + })?; + + let response_bytes = extract_response_bytes(onmessage)?; + + // Read and process the response + let response_buf = read_response(response_bytes).await?; + // Now process the response using the request_id + process_response(response_buf, request_id).await +} + +/// Creates a length buffer to represent the size of the payload. +/// +/// This function initializes a buffer of fixed size, which is used to store +/// the length of the payload in little-endian byte order. It ensures that +/// the length can be correctly interpreted +/// +/// # Parameters +/// +/// - `payload_length`: The length of the payload to be encoded in the buffer, +/// represented as a `u32`. This value is converted to a byte array and +/// stored in the buffer. +/// +/// # Returns +/// +/// Returns a `Vec` containing the byte representation of the payload length, +/// with the length stored in little-endian format. The size of the vector is +/// determined by `LENGTH_FIELD_SIZE`. +#[cfg(target_arch = "wasm32")] +fn create_length_buffer(payload_length: u32) -> Vec { + let mut length_buffer = vec![0; LENGTH_FIELD_SIZE]; + length_buffer[..LENGTH_FIELD_SIZE].copy_from_slice(&payload_length.to_le_bytes()); + length_buffer +} + +/// Sends the length of the payload followed by the payload itself over the WebSocket. +/// +/// This function retrieves the WebSocket's `send` method and sends the specified +/// length and payload. It first sends the length of the payload encoded as a +/// `Uint8Array`, and then, upon successful transmission, it invokes the +/// `send_payload` function to send the actual payload data. +/// +/// # Parameters +/// +/// - `web_socket`: A reference to the `WebSocket` instance to which the data will be sent. +/// - `length_js_value`: A reference to a `Uint8Array` that contains the length of the payload, +/// which will be sent first. +/// - `payload`: A reference to the `Vec` containing the actual payload data +/// to be sent after the length. +/// - `resolve`: A reference to a JavaScript function that will be called to resolve the +/// promise if the send operation is successful. +/// - `reject`: A reference to a JavaScript function that will be called to reject the +/// promise if an error occurs during the send operation. +/// +/// # Behavior +/// +/// - If the WebSocket's `send` method is successfully retrieved and called with the +/// `length_js_value`, the function proceeds to send the actual payload using the +/// `send_payload` function. +/// - If an error occurs while sending the length, it logs the error and calls the +/// `reject` function, passing the error as an argument. +/// - If the `send` method cannot be found, it logs an error message and calls the +/// `reject` function with the corresponding error. +#[cfg(target_arch = "wasm32")] +fn send_length_and_payload( + web_socket: &WebSocket, + length_js_value: &js_sys::Uint8Array, + payload: &Vec, + resolve: &js_sys::Function, + reject: &js_sys::Function, +) { + let send_func_result = js_sys::Reflect::get(web_socket, &JsString::from("send")) + .and_then(|send_func| send_func.dyn_into::()); + + match send_func_result { + Ok(send_func) => { + if let Err(e) = send_func.call1(web_socket, length_js_value) { + log(&format!("Failed to send length buffer: {:?}", e)); + reject.call1(&JsValue::NULL, &e).unwrap(); + } else { + // log("Length buffer sent successfully, now sending payload."); + send_payload(&send_func, web_socket, payload, resolve, reject); + } + } + Err(e) => { + log("Failed to find WebSocket send function."); + reject.call1(&JsValue::NULL, &e).unwrap(); // Use the cloned reject + } + } +} + +/// Sends the payload data over the specified WebSocket. +/// +/// This function converts the provided payload into a `Uint8Array` and uses the +/// provided `send_func` to send it over the WebSocket. If the send operation is +/// successful, it sets up a message handler; otherwise, it logs the error and +/// calls the provided reject function. +/// +/// # Parameters +/// +/// - `send_func`: A reference to the JavaScript function used to send data over the WebSocket. +/// - `web_socket`: A reference to the `WebSocket` instance through which the payload will be sent. +/// - `payload`: A reference to a `Vec` containing the data to be sent. +/// - `resolve`: A reference to a JavaScript function that will be called to resolve the +/// promise if the send operation is successful. +/// - `reject`: A reference to a JavaScript function that will be called to reject the +/// promise if an error occurs during the send operation. +#[cfg(target_arch = "wasm32")] +fn send_payload( + send_func: &js_sys::Function, + web_socket: &WebSocket, + payload: &Vec, + resolve: &js_sys::Function, + reject: &js_sys::Function, +) { + let payload_array = js_sys::Uint8Array::from(payload.as_slice()); + if let Err(e) = send_func.call1(web_socket, &payload_array) { + log(&format!("Failed to send payload: {:?}", e)); + reject.call1(&JsValue::NULL, &e).unwrap(); + } else { + // log("Payload sent successfully, setting up message handler."); + setup_message_handler(web_socket, resolve, reject); + } +} + +/// Sets up message and error handlers for the specified WebSocket. +/// +/// This function configures the WebSocket to handle incoming messages and errors. +/// It defines two closures: one for handling WebSocket errors and another for +/// processing incoming messages. If an error occurs, it logs the error and calls +/// the provided reject function. When a message is received, it calls the +/// provided resolve function to process the message. +/// +/// # Parameters +/// +/// - `web_socket`: A reference to the `WebSocket` instance for which the handlers are being set up. +/// - `resolve`: A reference to a JavaScript function that will be called to resolve the +/// promise when a message is received. +/// - `reject`: A reference to a JavaScript function that will be called to reject the +/// promise if an error occurs during WebSocket communication. +#[cfg(target_arch = "wasm32")] +fn setup_message_handler( + web_socket: &WebSocket, + resolve: &js_sys::Function, + reject: &js_sys::Function, +) { + let onerror = { + let reject = reject.clone(); // Clone for use in onerror + Closure::wrap(Box::new(move |event: wasm_bindgen::JsValue| { + let error_msg = event + .as_string() + .unwrap_or_else(|| "WebSocket error".to_string()); + log(&format!("WebSocket error: {}", error_msg)); + reject + .call1(&JsValue::NULL, &JsValue::from_str(&error_msg)) + .unwrap(); + }) as Box) + }; + + web_socket.set_onerror(Some(onerror.as_ref().unchecked_ref())); + onerror.forget(); // Prevent memory leak by forgetting the closure + + let onmessage = { + let resolve = resolve.clone(); // Clone for use in onmessage + Closure::wrap(Box::new(move |event: MessageEvent| { + // log("Message received from WebSocket."); + handle_message(event, resolve.clone()); + }) as Box) + }; + web_socket.set_onmessage(Some(onmessage.as_ref().unchecked_ref())); + onmessage.forget(); // Prevent memory leak by forgetting the closure +} + +/// Handles incoming messages from the WebSocket. +/// +/// This function is triggered when a message event occurs. It extracts the data +/// from the message event, which is expected to be a `Blob`. It then uses a +/// `FileReader` to read the `Blob` data as an `ArrayBuffer`. Upon successful +/// reading, it converts the data to a `Uint8Array` and resolves the provided +/// function with the binary response. +/// +/// # Parameters +/// +/// - `event`: The `MessageEvent` that contains the incoming data from the WebSocket. +/// - `resolve`: A reference to a JavaScript function that will be called with the +/// binary response once the `Blob` data has been successfully read. +#[cfg(target_arch = "wasm32")] +fn handle_message(event: MessageEvent, resolve: js_sys::Function) { + // Convert the event data to Blob + let data: web_sys::Blob = event.data().dyn_into::().unwrap(); + + // Create a FileReader to read the Blob + let file_reader = web_sys::FileReader::new().unwrap(); // Create FileReader + let resolve = resolve.clone(); // Clone for use in onload + + // Set up the onload closure + let onload = { + let file_reader = file_reader.clone(); // Clone here + Closure::wrap(Box::new(move |_: web_sys::ProgressEvent| { + // log("Blob read successfully, converting to Uint8Array."); + let result = file_reader.result().unwrap(); + let array_buffer = result.dyn_into::().unwrap(); + let uint8_array = js_sys::Uint8Array::new(&array_buffer); + let response_bytes = uint8_array.to_vec(); + // log(&format!("Received bytes: {:?}", response_bytes)); + + // Resolve with binary response + resolve + .call1( + &JsValue::NULL, + &wasm_bindgen::JsValue::from_serde(&response_bytes).unwrap_or_default(), + ) + .unwrap(); + }) as Box) + }; + + // Set up the onload event for the FileReader + file_reader.set_onload(Some(onload.as_ref().unchecked_ref())); + file_reader.read_as_array_buffer(&data).unwrap(); // Ensure read call + onload.forget(); // Prevent memory leak by forgetting the closure +} + +/// Extracts response bytes from a JavaScript value. +/// +/// This function takes a JavaScript value expected to be an `Array`, converts it +/// into a Rust vector of bytes, and returns it. If the conversion fails, it returns +/// an `Error` indicating that the expected format was not met. +/// +/// # Parameters +/// +/// - `onmessage`: A `JsValue` that should contain the response data in the form of a JavaScript `Array`. +/// +/// # Returns +/// +/// A `Result, Error>`, where the `Ok` variant contains the extracted bytes as a `Vec`, +/// and the `Err` variant contains an `Error` if the conversion fails. +#[cfg(target_arch = "wasm32")] +fn extract_response_bytes(onmessage: JsValue) -> Result, Error> { + let response_bytes = onmessage + .dyn_into::() + .map_err(|_| Error::Response("Expected Array format for TCP response data".to_string()))? + .to_vec() + .into_iter() + .map(|val| val.as_f64().unwrap_or(0.0) as u8) + .collect::>(); + Ok(response_bytes) +} + +/// Reads and processes a response from a byte vector, extracting the payload +/// based on a length prefix. +/// +/// This asynchronous function reads a response in the form of a byte vector +/// that includes a length prefix. The length prefix indicates the size of the +/// actual payload that follows. The function validates the response format +/// and returns the extracted payload if the format is correct. +/// +/// # Parameters +/// +/// - `response_bytes`: A `Vec` containing the raw bytes of the response +/// received from a server. The first `LENGTH_FIELD_SIZE` bytes represent +/// the length of the subsequent payload. +/// +/// # Returns +/// +/// This function returns a `Result` that, on success, contains a `Vec` +/// representing the extracted payload. On failure, it contains an `Error`. +/// +/// # Errors +/// +/// This function may return an `Error` if: +/// - The input `response_bytes` does not contain enough bytes to read the +/// length prefix. +/// - The specified length of the payload exceeds the total number of bytes +/// available, indicating that the response is incomplete. +/// +/// # Notes +/// +/// Ensure that the `LENGTH_FIELD_SIZE` constant is properly defined to +/// match the expected size of the length prefix (4 bytes for +/// a `u32` for Casper binary protocol). This function is only compiled for the `wasm32` target, +/// making it suitable for WebAssembly applications where binary data +/// processing is required. +#[cfg(target_arch = "wasm32")] +async fn read_response(response_bytes: Vec) -> Result, Error> { + // Ensure we have enough bytes for the length field + if response_bytes.len() < LENGTH_FIELD_SIZE { + return Err(Error::Response( + "Insufficient data for length prefix".to_string(), + )); + } + + // Read the length prefix (first 4 bytes) as a little-endian u32 + let response_length = + u32::from_le_bytes(response_bytes[0..LENGTH_FIELD_SIZE].try_into().unwrap()) as usize; + + // Ensure the buffer is large enough for the specified length + if response_bytes.len() < LENGTH_FIELD_SIZE + response_length { + return Err(Error::Response("Incomplete response data".to_string())); + } + + // Extract the actual response payload + let response_buf = + response_bytes[LENGTH_FIELD_SIZE..LENGTH_FIELD_SIZE + response_length].to_vec(); + + Ok(response_buf) +} + +/// Sends a binary request to a specified node address, either via TCP or WebSocket. +/// +/// This asynchronous function generates a unique request ID, encodes the given +/// binary request into a payload, and then sends the request to the specified +/// `node_address`. The method of transmission depends on the environment: +/// - In Node.js, it uses raw TCP connections via the `net` module. +/// - In non-Node.js environments (typically browsers), it uses WebSockets. +/// Note that browsers have CORS (Cross-Origin Resource Sharing) restrictions, +/// so the WebSocket requests should/may be addressed to a WebSocket proxy that +/// redirects the requests to the node's binary port. +/// +/// # Parameters +/// +/// - `node_address`: A `&str` representing the address of the node to which +/// the request will be sent. This should include the host and port (e.g., +/// "localhost:28101"). +/// - `request`: A `BinaryRequest` instance containing the data to be sent. +/// +/// # Returns +/// +/// This function returns a `Result` that, on success, contains a `BinaryResponseAndRequest` +/// indicating the response received from the node. On failure, it returns an `Error`. +/// +/// # Errors +/// +/// This function may return an `Error` if: +/// - There is an issue serializing the request into a binary format. +/// - The connection fails when trying to open a TCP connection in Node.js. +/// - The WebSocket connection cannot be created in non-Node.js environments. +/// - There are issues handling the WebSocket connection. +/// +/// # Notes +/// +/// This function is only compiled for the `wasm32` target, making it suitable +/// for WebAssembly applications where communication with a node server is required. +#[cfg(target_arch = "wasm32")] +pub(crate) async fn send_request( + node_address: &str, + request: BinaryRequest, +) -> Result { + let request_id = rand::thread_rng().gen::(); + let payload = encode_request(&request, Some(request_id)) + .map_err(|e| Error::Serialization(format!("Failed to serialize request: {}", e)))?; + + thread_local! { + static WS: RefCell> = const { RefCell::new(None) }; + } + + if is_node() { + // In Node.js, use raw TCP with the `net` module for direct connection + let tcp_result = open_tcp_connection(node_address, payload.clone(), request_id).await; + match tcp_result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Response(format!("TCP connection failed: {:?}", e))), + } + } else { + let web_socket_url = if node_address.starts_with("ws://") { + node_address.to_string() + } else { + format!("ws://{}", node_address) + }; + + // Check if WebSocket is already initialized and if it is open + let mut web_socket = WS.with(|ws| ws.borrow_mut().clone()); + + if web_socket.is_none() + || web_socket.as_ref().unwrap().ready_state() == WebSocket::CLOSED + || web_socket.as_ref().unwrap().ready_state() == WebSocket::CLOSING + { + // Create a new WebSocket if it doesn't exist or is not open + web_socket = Some(WebSocket::new(&web_socket_url).map_err(|e| { + Error::WebSocketCreation(format!("Failed to create WebSocket: {:?}", e)) + })?); + + // Setup error and close event handlers to update WebSocket state + let web_socket_clone = web_socket.as_ref().unwrap().clone(); + // Save the new WebSocket in the thread-local variable + WS.with(|ws| { + *ws.borrow_mut() = Some(web_socket_clone); + }); + } + + let web_socket_ref = web_socket.as_ref().unwrap(); + + if web_socket_ref.ready_state() == WebSocket::CONNECTING + || web_socket_ref.ready_state() == WebSocket::OPEN + { + let response = handle_websocket_connection(web_socket_ref, payload, request_id).await?; + Ok(response) + } else { + Err(Error::WebSocketSend("WebSocket is not open".to_string())) + } + } +} diff --git a/binary-port-access/src/communication/wasm32/node_tcp_helper.rs b/binary-port-access/src/communication/wasm32/node_tcp_helper.rs new file mode 100644 index 0000000..0237e73 --- /dev/null +++ b/binary-port-access/src/communication/wasm32/node_tcp_helper.rs @@ -0,0 +1,67 @@ +#[cfg(target_arch = "wasm32")] +pub(crate) const NODE_TCP_HELPER: &str = r#" +(async () => {{ + try {{ + const net = require('net'); + const client = new net.Socket(); + const payload = Buffer.from({buffer_payload}); + const host = '{host}'; + const port = '{port}'; + + return new Promise((resolve, reject) => {{ + // console.log('Connecting to TCP server at', host, port); + + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeUInt32LE(payload.length); + client.connect(parseInt(port), host, () => {{ + // console.log('Connected to TCP server'); + + // First, send the length of the payload + client.write(lengthBuffer, (err) => {{ + if (err) {{ + console.error('Error sending length:', err.message); + client.destroy(); + return; + }} + // console.log('Length of payload sent'); + + // Now, send the actual payload + client.write(payload, (err) => {{ + if (err) {{ + console.error('Error sending payload:', err.message); + }} else {{ + // console.log('Payload sent'); + }} + }}); + }}); + }}); + client.on('data', (data) => {{ + // console.log('Data received from server:', data); + resolve(data); + client.destroy(); // Close connection after receiving response + }}); + client.on('error', (err) => {{ + console.error('TCP connection error:', err.message); + reject(new Error('TCP connection error: ' + err.message)); + }}); + client.on('close', () => {{ + // console.log('TCP connection closed'); + }}); + }}); + }} catch (err) {{ + console.error('Error in TCP script:', err.message); + throw new Error('Script execution error: ' + err.message); + }} + }} +)();"#; + +pub(crate) fn sanitize_input(input: &str) -> String { + input + .replace("\\", "\\\\") // Escape backslashes + .replace("'", "\\'") // Escape single quotes + .replace("\"", "\\\"") // Escape double quotes + .replace("\n", "\\n") // Escape newlines + .replace("\r", "\\r") // Escape carriage returns + .replace("<", "\\<") // Escape less than + .replace(">", "\\>") // Escape greater than +} diff --git a/binary-port-access/src/error.rs b/binary-port-access/src/error.rs new file mode 100644 index 0000000..052c908 --- /dev/null +++ b/binary-port-access/src/error.rs @@ -0,0 +1,67 @@ +use casper_types::bytesrepr; +use thiserror::Error; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; + +#[derive(Error, Debug)] +/// Errors +pub enum Error { + /// Bytesrepr serialization error. + #[error(transparent)] + Bytesrepr(#[from] bytesrepr::Error), + + /// IO error. + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Error in the binary port. + #[error(transparent)] + BinaryPort(#[from] casper_binary_port::Error), + + /// Error when handling the response from the binary port. + #[error("Failed to handle response: {0}")] + Response(String), + + /// Error related to network timeout. + #[error("Failed to handle network response: {0}")] + TimeoutError(String), + + /// Error related to http request. + #[error("Failed to send http request: {0}")] + HttpRequest(String), + + /// Error related to http response. + #[error("Failed to handle http response: {0}")] + HttpResponse(String), + + /// WebSocket creation error. + #[error("Failed to create WebSocket: {0}")] + WebSocketCreation(String), + + /// WebSocket creation error. + #[error("Failed to create WebSocket: {0}")] + WebSocketSend(String), + + /// JavaScript-related error. + #[error("JavaScript error: {0}")] + JsError(String), + + /// General serialization error. + #[error("Serialization error: {0}")] + Serialization(String), + + /// Generic error for other cases. + #[error("Other error: {0}")] + Other(String), +} + +#[cfg(target_arch = "wasm32")] +impl From for Error { + fn from(js_value: JsValue) -> Self { + Error::JsError( + js_value + .as_string() + .unwrap_or_else(|| "Unknown JavaScript error".to_string()), + ) + } +} diff --git a/binary_port_access/src/lib.rs b/binary-port-access/src/lib.rs similarity index 80% rename from binary_port_access/src/lib.rs rename to binary-port-access/src/lib.rs index 3c00027..48fc938 100644 --- a/binary_port_access/src/lib.rs +++ b/binary-port-access/src/lib.rs @@ -1,8 +1,9 @@ #![deny(missing_docs)] //! This crate provides a high-level API for interacting with a Casper node's binary port interface. -use communication::parse_response; -use thiserror::Error; +mod communication; +mod error; +mod utils; use casper_binary_port::{ BinaryRequest, ConsensusStatus, ConsensusValidatorChanges, EraIdentifier, @@ -15,22 +16,24 @@ use casper_types::{ BlockSynchronizerStatus, ChainspecRawBytes, Digest, EraId, GlobalStateIdentifier, Key, NextUpgrade, Peers, ProtocolVersion, PublicKey, SignedBlock, Transaction, TransactionHash, }; - -mod communication; -mod error; -pub(crate) mod utils; +use communication::common::parse_response; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use communication::common::send_request; +#[cfg(target_arch = "wasm32")] +pub(crate) use communication::wasm32::send_request; pub use error::Error; +use thiserror::Error; use utils::{ check_error_code, delegator_reward_by_era_identifier, global_state_item_by_state_identifier, - validator_reward_by_era_identifier, + make_information_get_request, validator_reward_by_era_identifier, }; /// Returns the latest switch block header. pub async fn latest_switch_block_header(node_address: &str) -> Result, Error> { let request = - utils::make_information_get_request(InformationRequestTag::LatestSwitchBlockHeader, &[])?; - let response = communication::send_request(node_address, request).await?; + make_information_get_request(InformationRequestTag::LatestSwitchBlockHeader, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -38,11 +41,11 @@ pub async fn latest_switch_block_header(node_address: &str) -> Result Result, Error> { let block_id: Option = None; - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::BlockHeader, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -53,11 +56,11 @@ pub async fn block_header_by_height( height: u64, ) -> Result, Error> { let block_id = Some(BlockIdentifier::Height(height)); - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::BlockHeader, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -68,11 +71,11 @@ pub async fn block_header_by_hash( hash: BlockHash, ) -> Result, Error> { let block_id = Some(BlockIdentifier::Hash(hash)); - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::BlockHeader, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -80,11 +83,11 @@ pub async fn block_header_by_hash( /// Returns the latest block along with signatures. pub async fn latest_signed_block(node_address: &str) -> Result, Error> { let block_id: Option = None; - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::SignedBlock, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -95,11 +98,11 @@ pub async fn signed_block_by_height( height: u64, ) -> Result, Error> { let block_id = Some(BlockIdentifier::Height(height)); - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::SignedBlock, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -110,11 +113,11 @@ pub async fn signed_block_by_hash( hash: BlockHash, ) -> Result, Error> { let block_id = Some(BlockIdentifier::Hash(hash)); - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::SignedBlock, block_id.to_bytes()?.as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -127,7 +130,7 @@ pub async fn transaction_by_hash( hash: TransactionHash, with_finalized_approvals: bool, ) -> Result, Error> { - let request = utils::make_information_get_request( + let request = make_information_get_request( InformationRequestTag::Transaction, hash.to_bytes()? .into_iter() @@ -135,15 +138,15 @@ pub async fn transaction_by_hash( .collect::>() .as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } /// Returns the peer list. pub async fn peers(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::Peers, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::Peers, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let peers = parse_response::(response.response())?; peers.ok_or_else(|| Error::Response("unable to read peers".to_string())) @@ -151,8 +154,8 @@ pub async fn peers(node_address: &str) -> Result { /// Returns the node uptime. pub async fn uptime(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::Uptime, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::Uptime, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let uptime = parse_response::(response.response())?; uptime.ok_or_else(|| Error::Response("unable to read uptime".to_string())) @@ -160,8 +163,8 @@ pub async fn uptime(node_address: &str) -> Result { /// Returns the last progress as recorded by the node. pub async fn last_progress(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::LastProgress, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::LastProgress, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let last_progress = parse_response::(response.response())?; last_progress.ok_or_else(|| Error::Response("unable to read last progress".to_string())) @@ -169,8 +172,8 @@ pub async fn last_progress(node_address: &str) -> Result { /// Returns the current reactor state. pub async fn reactor_state(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::ReactorState, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::ReactorState, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let reactor_state = parse_response::(response.response())?; reactor_state.ok_or_else(|| Error::Response("unable to read last reactor state".to_string())) @@ -178,8 +181,8 @@ pub async fn reactor_state(node_address: &str) -> Result Result { - let request = utils::make_information_get_request(InformationRequestTag::NetworkName, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::NetworkName, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let network_name = parse_response::(response.response())?; network_name.ok_or_else(|| Error::Response("unable to read last network name".to_string())) @@ -190,8 +193,8 @@ pub async fn consensus_validator_changes( node_address: &str, ) -> Result { let request = - utils::make_information_get_request(InformationRequestTag::ConsensusValidatorChanges, &[])?; - let response = communication::send_request(node_address, request).await?; + make_information_get_request(InformationRequestTag::ConsensusValidatorChanges, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let consensus_validator_changes = parse_response::(response.response())?; @@ -205,8 +208,8 @@ pub async fn block_synchronizer_status( node_address: &str, ) -> Result { let request = - utils::make_information_get_request(InformationRequestTag::BlockSynchronizerStatus, &[])?; - let response = communication::send_request(node_address, request).await?; + make_information_get_request(InformationRequestTag::BlockSynchronizerStatus, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let block_synchronizer_status = parse_response::(response.response())?; block_synchronizer_status @@ -215,9 +218,8 @@ pub async fn block_synchronizer_status( /// Returns the available block range. pub async fn available_block_range(node_address: &str) -> Result { - let request = - utils::make_information_get_request(InformationRequestTag::AvailableBlockRange, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::AvailableBlockRange, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let available_block_range = parse_response::(response.response())?; available_block_range @@ -226,16 +228,16 @@ pub async fn available_block_range(node_address: &str) -> Result Result, Error> { - let request = utils::make_information_get_request(InformationRequestTag::NextUpgrade, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::NextUpgrade, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } /// Returns the current status of the consensus. pub async fn consensus_status(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::ConsensusStatus, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::ConsensusStatus, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let consensus_status = parse_response::(response.response())?; consensus_status @@ -245,9 +247,8 @@ pub async fn consensus_status(node_address: &str) -> Result Result { - let request = - utils::make_information_get_request(InformationRequestTag::ChainspecRawBytes, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::ChainspecRawBytes, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let chainspec_raw_bytes = parse_response::(response.response())?; chainspec_raw_bytes @@ -256,8 +257,8 @@ pub async fn chainspec_raw_bytes(node_address: &str) -> Result Result { - let request = utils::make_information_get_request(InformationRequestTag::NodeStatus, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::NodeStatus, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let node_status = parse_response::(response.response())?; node_status.ok_or_else(|| Error::Response("unable to read last node status".to_string())) @@ -351,7 +352,7 @@ pub async fn read_record( key: &[u8], ) -> Result, Error> { let request = utils::make_record_request(record_id, key); - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; Ok(response.response().payload().into()) } @@ -407,7 +408,7 @@ pub async fn try_accept_transaction( transaction: Transaction, ) -> Result<(), Error> { let request = BinaryRequest::TryAcceptTransaction { transaction }; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response) } @@ -417,7 +418,7 @@ pub async fn try_speculative_execution( transaction: Transaction, ) -> Result { let request = BinaryRequest::TrySpeculativeExec { transaction }; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; @@ -429,8 +430,8 @@ pub async fn try_speculative_execution( /// Returns the protocol version. pub async fn protocol_version(node_address: &str) -> Result { - let request = utils::make_information_get_request(InformationRequestTag::ProtocolVersion, &[])?; - let response = communication::send_request(node_address, request).await?; + let request = make_information_get_request(InformationRequestTag::ProtocolVersion, &[])?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; let protocol_version = parse_response::(response.response())?; protocol_version.ok_or_else(|| Error::Response("unable to read protocol version".to_string())) diff --git a/binary_port_access/src/utils.rs b/binary-port-access/src/utils.rs similarity index 83% rename from binary_port_access/src/utils.rs rename to binary-port-access/src/utils.rs index edeb240..962a33b 100644 --- a/binary_port_access/src/utils.rs +++ b/binary-port-access/src/utils.rs @@ -3,13 +3,16 @@ use casper_binary_port::{ GlobalStateQueryResult, GlobalStateRequest, InformationRequest, InformationRequestTag, RecordId, RewardResponse, }; -use casper_types::{bytesrepr::ToBytes, GlobalStateIdentifier, Key, PublicKey}; - -use crate::{ - communication::{self, parse_response}, - Error, +use casper_types::{ + bytesrepr::ToBytes, system::auction::DelegatorKind, GlobalStateIdentifier, Key, PublicKey, }; +#[cfg(not(target_arch = "wasm32"))] +use crate::communication::common::send_request; +#[cfg(target_arch = "wasm32")] +use crate::communication::wasm32::send_request; +use crate::{communication::common::parse_response, Error}; + pub(crate) fn make_information_get_request( tag: InformationRequestTag, key: &[u8], @@ -37,12 +40,12 @@ pub(crate) async fn delegator_reward_by_era_identifier( InformationRequest::Reward { era_identifier: Some(era_identifier), validator: Box::new(validator_key), - delegator: Some(Box::new(delegator_key)), + delegator: Some(Box::new(DelegatorKind::PublicKey(delegator_key))), } .to_bytes()? .as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } @@ -62,7 +65,7 @@ pub(crate) async fn validator_reward_by_era_identifier( .to_bytes()? .as_slice(), )?; - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; parse_response::(response.response()) } @@ -78,7 +81,7 @@ pub(crate) async fn global_state_item_by_state_identifier( }; let global_state_request = GlobalStateRequest::new(global_state_identifier, qualifier); let request = BinaryRequest::Get(GetRequest::State(Box::new(global_state_request))); - let response = communication::send_request(node_address, request).await?; + let response = send_request(node_address, request).await?; check_error_code(&response)?; parse_response::(response.response()) } diff --git a/binary_port_access/Cargo.toml b/binary_port_access/Cargo.toml deleted file mode 100644 index f1be978..0000000 --- a/binary_port_access/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "casper-binary-port-access" -version = "0.1.0" -edition = "2021" -authors = ["Rafał Chabowski "] -description = "Library for accessing Casper binary port." -license = "Apache-2.0" - -[dependencies] -casper-types = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } -casper-binary-port = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } -thiserror = "1.0.58" -tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread", "net"] } -tokio-util = { version = "0.6.4", features = ["codec"] } -futures = "0.3.31" diff --git a/binary_port_access/src/communication.rs b/binary_port_access/src/communication.rs deleted file mode 100644 index 0516c13..0000000 --- a/binary_port_access/src/communication.rs +++ /dev/null @@ -1,72 +0,0 @@ -use casper_binary_port::{ - BinaryMessage, BinaryMessageCodec, BinaryRequest, BinaryRequestHeader, BinaryResponse, - BinaryResponseAndRequest, PayloadEntity, -}; -use casper_types::{ - bytesrepr::{self, FromBytes, ToBytes}, - ProtocolVersion, -}; -use futures::{SinkExt, StreamExt}; -use tokio::net::TcpStream; -use tokio_util::codec::Framed; - -use crate::Error; - -// TODO[RC]: Do not hardcode this. -pub(crate) const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(2, 0, 0); - -async fn connect_to_node( - node_address: &str, -) -> Result, Error> { - let stream = TcpStream::connect(node_address).await?; - Ok(Framed::new(stream, BinaryMessageCodec::new(4_194_304))) -} - -fn encode_request(req: &BinaryRequest) -> Result, bytesrepr::Error> { - let header = BinaryRequestHeader::new(SUPPORTED_PROTOCOL_VERSION, req.tag(), 0); - let mut bytes = Vec::with_capacity(header.serialized_length() + req.serialized_length()); - header.write_bytes(&mut bytes)?; - req.write_bytes(&mut bytes)?; - Ok(bytes) -} - -// TODO[RC]: Into "communication" module -pub(crate) async fn send_request( - node_address: &str, - request: BinaryRequest, -) -> Result { - let payload = - BinaryMessage::new(encode_request(&request).expect("should always serialize a request")); - - let mut client = connect_to_node(node_address).await?; - client.send(payload).await?; - let maybe_response = client.next().await; - - let Some(response) = maybe_response else { - return Err(Error::Response("empty response".to_string())); - }; - - let response = response?; - let payload = response.payload(); - Ok(bytesrepr::deserialize_from_slice(payload)?) -} - -pub(crate) fn parse_response( - response: &BinaryResponse, -) -> Result, Error> { - match response.returned_data_type_tag() { - Some(found) if found == u8::from(A::RESPONSE_TYPE) => { - // Verbose: Print length of payload - let payload = response.payload(); - let _payload_length = payload.len(); - // TODO[GR] use tracing::info! instead of dbg! - // dbg!(_payload_length); - - Ok(Some(bytesrepr::deserialize_from_slice(payload)?)) - } - Some(other) => Err(Error::Response(format!( - "unsupported response type: {other}" - ))), - _ => Ok(None), - } -} diff --git a/binary_port_access/src/error.rs b/binary_port_access/src/error.rs deleted file mode 100644 index 0d71bf5..0000000 --- a/binary_port_access/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use casper_types::bytesrepr; -use thiserror::Error; - -/// Possible errors that can occur when interacting with the binary port. -#[derive(Error, Debug)] -pub enum Error { - /// Bytesrepr serialization error. - #[error(transparent)] - Bytesrepr(#[from] bytesrepr::Error), - /// IO error. - #[error(transparent)] - Io(#[from] std::io::Error), - /// Error in the binary port. - #[error(transparent)] - BinaryPort(#[from] casper_binary_port::Error), - /// Error when handling the response from the binary port. - #[error("failed to handle response: {0}")] - Response(String), -}