diff --git a/.github/workflows/typescript.yml b/.github/workflows/typescript.yml index 2df0f42c..daecccb7 100644 --- a/.github/workflows/typescript.yml +++ b/.github/workflows/typescript.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'npm' cache-dependency-path: js/package-lock.json diff --git a/.gitignore b/.gitignore index 74f458c1..85002940 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ test-ledger/ result .direnv .blacklist +logs diff --git a/CHANGELOG.md b/CHANGELOG.md index 95f59ec8..d49ce64f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,24 @@ Race Protocol: A multi-chain infrastructure for asymmetric competitive games # Master(Unreleased) +## Breaking Changes +- Facade: Argument format updated. Now it receives `-g` for game, and `-b` for bundle. + +### Replace addr with id in games +- Now we use `id: u64` instead of `addr: string` in games. The value of `id` is taken from the `access_version` which is incremental and unique in game. This update helps us to improve the performance and reduce the sizes of checkpoints and states. +- API: `Effect::count_players` and `Effect::count_servers` has been removed, use `Effect::count_nodes` to count how many nodes are involved in the randomization. +- SDK: `AppClient.getProfiles` now receives both `addr: string` and `id: bigint`. +- TestKit: Add `TestClient::join`. + ## Enhancements - SDK: Remove dependency `crypto` to support NodeJS runtime. - CLI: Print `checkpoint` in hex format in command `game-info`. +- TestKit: Add `handle_dispatch_until_no_events` to TestHandler. ## Features - CLI: Update `publish` command. Now it receives the path to the WASM bundle instead of the Arweave URL to solana metadata. - Add optional `createProfileIfNeeded` to join options. +- SDK: Add `recipientClaim` and its solana implementation. ## Fixes - Transactor: Improve the retry mechanism for settle. diff --git a/Cargo.lock b/Cargo.lock index c3bbfed1..8b80e687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,25 +29,14 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ + "crypto-common", "generic-array", ] -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.2" @@ -55,19 +44,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] [[package]] name = "aes-gcm-siv" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" dependencies = [ "aead", - "aes 0.7.5", - "cipher 0.3.0", + "aes", + "cipher", "ctr", "polyval", "subtle", @@ -85,6 +74,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -118,6 +119,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -127,15 +134,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "0.3.1" @@ -187,15 +185,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "ark-bn254" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea691771ebbb28aea556c044e2e5c5227398d840cee0c34d4d20fa8eb2689e8c" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" dependencies = [ "ark-ec", "ark-ff", @@ -204,95 +202,121 @@ dependencies = [ [[package]] name = "ark-ec" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea978406c4b1ca13c2db2373b05cc55429c3575b8b21f1b9ee859aa5b03dd42" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ "ark-ff", + "ark-poly", "ark-serialize", "ark-std", "derivative", + "hashbrown 0.13.2", + "itertools 0.10.5", "num-traits", "zeroize", ] [[package]] name = "ark-ff" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" dependencies = [ "ark-ff-asm", "ark-ff-macros", "ark-serialize", "ark-std", "derivative", - "num-bigint 0.4.3", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.6", "num-traits", "paste", - "rustc_version 0.3.3", + "rustc_version", "zeroize", ] [[package]] name = "ark-ff-asm" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" dependencies = [ - "quote 1.0.26", + "quote", "syn 1.0.109", ] [[package]] name = "ark-ff-macros" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" dependencies = [ - "num-bigint 0.4.3", + "num-bigint 0.4.6", "num-traits", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + [[package]] name = "ark-serialize" -version = "0.3.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ + "ark-serialize-derive", "ark-std", - "digest 0.9.0", + "digest 0.10.7", + "num-bigint 0.4.6", ] [[package]] -name = "ark-std" -version = "0.3.0" +name = "ark-serialize-derive" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" dependencies = [ - "num-traits", - "rand 0.8.5", + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] -name = "array-bytes" -version = "1.4.1" +name = "ark-std" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad284aeb45c13f2fb4f084de4a420ebf447423bdf9386c0540ce33cb3ef4b8c" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] [[package]] name = "arrayref" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ascii" @@ -300,45 +324,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" -[[package]] -name = "asn1-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" -dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", - "displaydoc", - "nom", - "num-traits", - "rusticata-macros", - "thiserror", - "time 0.3.20", -] - -[[package]] -name = "asn1-rs-derive" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", - "synstructure", -] - -[[package]] -name = "asn1-rs-impl" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", -] - [[package]] name = "assert_matches" version = "1.5.0" @@ -347,9 +332,9 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "async-compression" -version = "0.3.15" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "brotli", "flate2", @@ -357,6 +342,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", + "zstd 0.13.0", + "zstd-safe 7.0.0", ] [[package]] @@ -368,15 +355,6 @@ dependencies = [ "event-listener", ] -[[package]] -name = "async-mutex" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" -dependencies = [ - "event-listener", -] - [[package]] name = "async-stream" version = "0.3.5" @@ -394,31 +372,20 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", -] - -[[package]] -name = "atty" -version = "0.2.14" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -456,15 +423,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "base64ct" -version = "1.1.1" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "beef" @@ -491,26 +458,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bitmaps" -version = "2.1.0" +name = "bitflags" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ - "typenum", + "serde", ] [[package]] name = "blake3" -version = "1.3.3" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -540,54 +507,78 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "borsh" -version = "0.9.3" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive 0.10.3", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ - "borsh-derive", - "hashbrown 0.11.2", + "borsh-derive 1.5.1", + "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate 0.1.5", - "proc-macro2 1.0.56", + "proc-macro2", "syn 1.0.109", ] +[[package]] +name = "borsh-derive" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +dependencies = [ + "once_cell", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.77", + "syn_derive", +] + [[package]] name = "borsh-derive-internal" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "borsh-schema-derive-internal" -version = "0.9.3" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "brotli" -version = "3.3.4" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -596,9 +587,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -606,9 +597,12 @@ dependencies = [ [[package]] name = "bs58" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] [[package]] name = "bstr" @@ -653,61 +647,52 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.4.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "caps" -version = "0.5.5" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" -dependencies = [ - "libc", - "thiserror", -] +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -727,6 +712,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -734,33 +725,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", + "windows-targets 0.52.6", ] [[package]] @@ -773,37 +754,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "atty", - "bitflags", - "clap_lex 0.2.4", - "indexmap", - "once_cell", - "strsim 0.10.0", - "termcolor", - "textwrap 0.16.0", -] - [[package]] name = "clap" version = "4.2.5" @@ -823,9 +773,9 @@ checksum = "0fdc5d93c358224b4d6867ef1356d740de2303e9892edc06c5340daeccd96bab" dependencies = [ "anstream", "anstyle", - "bitflags", - "clap_lex 0.4.1", - "strsim 0.10.0", + "bitflags 1.3.2", + "clap_lex", + "strsim", ] [[package]] @@ -835,18 +785,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" dependencies = [ "heck", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -886,15 +827,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.5" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode 0.3.6", "lazy_static", "libc", "unicode-width", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] @@ -917,23 +858,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "const-oid" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" - -[[package]] -name = "const-oid" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" - [[package]] name = "constant_time_eq" -version = "0.2.5" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "core-foundation" @@ -1027,7 +956,7 @@ dependencies = [ "cranelift-entity", "fxhash", "hashbrown 0.12.3", - "indexmap", + "indexmap 1.9.3", "log", "smallvec", ] @@ -1067,11 +996,10 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] @@ -1101,12 +1029,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -1121,6 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1157,11 +1083,11 @@ dependencies = [ [[package]] name = "ctr" -version = "0.8.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.3.0", + "cipher", ] [[package]] @@ -1199,10 +1125,10 @@ dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "scratch", - "syn 2.0.15", + "syn 2.0.77", ] [[package]] @@ -1217,9 +1143,9 @@ version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -1250,8 +1176,8 @@ checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -1263,10 +1189,10 @@ checksum = "2ea05d2fcb27b53f7a98faddaf5f2914760330ab7703adfc9df13332b42189f9" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.56", - "quote 1.0.26", - "strsim 0.10.0", - "syn 2.0.15", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.77", ] [[package]] @@ -1276,7 +1202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core 0.14.4", - "quote 1.0.26", + "quote", "syn 1.0.109", ] @@ -1287,18 +1213,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bfb82b62b1b8a2a9808fb4caf844ede819a76cfc23b2827d7f94eefb49551eb" dependencies = [ "darling_core 0.20.0", - "quote 1.0.26", - "syn 2.0.15", + "quote", + "syn 2.0.77", ] [[package]] name = "dashmap" -version = "5.4.0" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.12.3", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core", @@ -1306,32 +1232,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" - -[[package]] -name = "der" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" -dependencies = [ - "const-oid 0.7.1", -] - -[[package]] -name = "der-parser" -version = "8.2.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" -dependencies = [ - "asn1-rs", - "displaydoc", - "nom", - "num-bigint 0.4.3", - "num-traits", - "rusticata-macros", -] +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "derivation-path" @@ -1345,23 +1248,11 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] -[[package]] -name = "dialoguer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" -dependencies = [ - "console", - "shell-words", - "tempfile", - "zeroize", -] - [[package]] name = "digest" version = "0.9.0" @@ -1373,12 +1264,11 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid 0.9.2", "crypto-common", "subtle", ] @@ -1424,40 +1314,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "displaydoc" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", -] - -[[package]] -name = "dlopen" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" -dependencies = [ - "dlopen_derive", - "lazy_static", - "libc", - "winapi", -] - -[[package]] -name = "dlopen_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" -dependencies = [ - "libc", - "quote 0.6.13", - "syn 0.15.44", -] - [[package]] name = "eager" version = "0.1.0" @@ -1496,7 +1352,7 @@ dependencies = [ "derivation-path", "ed25519-dalek", "hmac 0.12.1", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -1537,11 +1393,11 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" +checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94" dependencies = [ - "enum-iterator-derive 1.2.0", + "enum-iterator-derive 1.4.0", ] [[package]] @@ -1550,32 +1406,20 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "enum-iterator-derive" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f93763ef7b0ae1c43c4d8eccc9d5848d84ad1a1d8ce61c421d1ac85a19d05" +checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", -] - -[[package]] -name = "enum_dispatch" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" -dependencies = [ - "once_cell", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -1594,23 +1438,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03e7b551eba279bf0fa88b83a46330168c1560a52a94f5126f892f0b364ab3e0" dependencies = [ "darling 0.14.4", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" @@ -1645,6 +1482,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.9.0" @@ -1693,18 +1542,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -1717,9 +1566,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -1727,15 +1576,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -1744,32 +1593,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" @@ -1783,9 +1632,9 @@ dependencies = [ [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -1861,8 +1710,8 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" dependencies = [ - "fallible-iterator", - "indexmap", + "fallible-iterator 0.2.0", + "indexmap 1.9.3", "stable_deref_trait", ] @@ -1953,7 +1802,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1971,55 +1820,55 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash", + "ahash 0.8.11", ] [[package]] -name = "hdrhistogram" -version = "7.5.2" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "byteorder", - "num-traits", + "ahash 0.8.11", ] [[package]] -name = "heck" -version = "0.4.1" +name = "hashlink" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "hdrhistogram" +version = "7.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "7f19b9f54f7c7f55e31401bb647626ce0cf0f67b0004982ce815b3ee72a02aa8" dependencies = [ - "libc", + "byteorder", + "num-traits", ] [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "heck" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -2028,10 +1877,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" [[package]] -name = "histogram" -version = "0.6.9" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" @@ -2049,7 +1898,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2087,9 +1936,9 @@ dependencies = [ [[package]] name = "http-range-header" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" @@ -2103,12 +1952,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.26" @@ -2135,10 +1978,11 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ + "futures-util", "http", "hyper", "log", @@ -2146,7 +1990,6 @@ dependencies = [ "rustls-native-certs", "tokio", "tokio-rustls", - "webpki-roots", ] [[package]] @@ -2194,47 +2037,42 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", ] [[package]] -name = "im" -version = "15.1.0" +name = "indexmap" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ - "bitmaps", - "rand_core 0.6.4", - "rand_xoshiro", - "rayon", - "serde", - "sized-chunks", - "typenum", - "version_check", + "autocfg", + "hashbrown 0.12.3", ] [[package]] name = "indexmap" -version = "1.9.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ - "autocfg", - "hashbrown 0.12.3", + "equivalent", + "hashbrown 0.14.5", ] [[package]] name = "indicatif" -version = "0.17.3" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" dependencies = [ "console", + "instant", "number_prefix", "portable-atomic", "unicode-width", @@ -2273,7 +2111,7 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "libc", "windows-sys 0.48.0", ] @@ -2286,11 +2124,12 @@ checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "iri-string" -version = "0.4.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0f7638c1e223529f1bfdc48c8b133b9e0b434094d1d28473161ee48b235f78" +checksum = "7f5f6c2df22c009ac44f6f1499308e7a3ac7ba42cd2378475cc691510e1eef1b" dependencies = [ - "nom", + "memchr", + "serde", ] [[package]] @@ -2299,7 +2138,7 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi 0.3.1", + "hermit-abi", "io-lifetimes", "rustix", "windows-sys 0.48.0", @@ -2314,6 +2153,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -2322,18 +2170,18 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -2355,9 +2203,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d291e3a5818a2384645fd9756362e6d89cf0541b0b916fa7702ea4a9833608e" +checksum = "8b971ce0f6cd1521ede485afc564b95b2c8e7079b9da41d4273bd9b55140a55d" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -2372,18 +2220,15 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965de52763f2004bc91ac5bcec504192440f0b568a5d621c59d9dbd6f886c3fb" +checksum = "7ca00d975eda834826b04ad57d4e690c67439bb51b02eb0f8b7e4c30fcef8ab9" dependencies = [ - "anyhow", "futures-channel", - "futures-timer", "futures-util", "gloo-net", "http", "jsonrpsee-core", - "jsonrpsee-types", "pin-project", "rustls-native-certs", "soketto", @@ -2392,21 +2237,19 @@ dependencies = [ "tokio-rustls", "tokio-util", "tracing", - "webpki-roots", + "webpki-roots 0.23.1", ] [[package]] name = "jsonrpsee-core" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e70b4439a751a5de7dd5ed55eacff78ebf4ffe0fc009cb1ebb11417f5b536b" +checksum = "b83cca7a5a7899eed8b2935d5f755c8c4052ad66ab5b328bd34ac2b3ffd3515f" dependencies = [ "anyhow", - "arrayvec", "async-lock", "async-trait", "beef", - "futures-channel", "futures-timer", "futures-util", "globset", @@ -2420,51 +2263,50 @@ dependencies = [ "soketto", "thiserror", "tokio", + "tokio-stream", "tracing", "wasm-bindgen-futures", ] [[package]] name = "jsonrpsee-http-client" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc345b0a43c6bc49b947ebeb936e886a419ee3d894421790c969cc56040542ad" +checksum = "6483fea826f62260a88a132fa750a47b40d4218d41e3391936579533c6c67509" dependencies = [ "async-trait", "hyper", "hyper-rustls", "jsonrpsee-core", "jsonrpsee-types", - "rustc-hash", "serde", "serde_json", "thiserror", "tokio", + "tower", "tracing", ] [[package]] name = "jsonrpsee-proc-macros" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa6da1e4199c10d7b1d0a6e5e8bd8e55f351163b6f4b3cbb044672a69bd4c1c" +checksum = "d814a21d9a819f8de1a41b819a263ffd68e4bb5f043d936db1c49b54684bde0a" dependencies = [ "heck", "proc-macro-crate 1.3.1", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "jsonrpsee-server" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb69dad85df79527c019659a992498d03f8495390496da2f07e6c24c2b356fc" +checksum = "4cf8b28bb9b0248d1ece5923c97355a4defc81c06b4b7a0097c61e412c716ca1" dependencies = [ - "futures-channel", "futures-util", - "http", "hyper", "jsonrpsee-core", "jsonrpsee-types", @@ -2480,9 +2322,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd522fe1ce3702fd94812965d7bb7a3364b1c9aba743944c5a00529aae80f8c" +checksum = "dd301ccc3e08718393432d1961539d78c4580dcca86014dfe6769c308b2c08b2" dependencies = [ "anyhow", "beef", @@ -2494,9 +2336,9 @@ dependencies = [ [[package]] name = "jsonrpsee-wasm-client" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77310456f43c6c89bcba1f6b2fc2a28300da7c341f320f5128f8c83cc63232d" +checksum = "85dc3aea8bbb844dacc45cd98d01f624e4485184149a045761888c2e5fa5a0c6" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -2505,9 +2347,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.16.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b83daeecfc6517cfe210df24e570fb06213533dfb990318fae781f4c7119dd9" +checksum = "89a69852133d549b07cb37ff2d0ec540eae0d20abb75ae923f5d39bc7536d987" dependencies = [ "http", "jsonrpsee-client-transport", @@ -2517,9 +2359,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ "cpufeatures", ] @@ -2538,19 +2380,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" - -[[package]] -name = "libloading" -version = "0.7.4" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libsecp256k1" @@ -2600,6 +2432,17 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -2617,9 +2460,9 @@ checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2627,12 +2470,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mach" @@ -2669,18 +2509,18 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.6.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" dependencies = [ "autocfg", ] [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -2713,12 +2553,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.6.2" @@ -2755,66 +2589,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7843ec2de400bcbc6a6328c958dc38e5359da6e93e72e37bc5246bf1ae776389" -[[package]] -name = "mpl-token-auth-rules" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24dcb2b0ec0e9246f6f035e0336ba3359c21f6928dfd90281999e2c8e8ab53eb" -dependencies = [ - "borsh", - "mpl-token-metadata-context-derive", - "num-derive", - "num-traits", - "rmp-serde", - "serde", - "shank", - "solana-program", - "solana-zk-token-sdk", - "thiserror", -] - -[[package]] -name = "mpl-token-metadata" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d414222c1b648d14559d6d1aaa5fa6f7dfdde3a26c76c331a87d6124aeb5e" -dependencies = [ - "arrayref", - "borsh", - "mpl-token-auth-rules", - "mpl-token-metadata-context-derive", - "mpl-utils", - "num-derive", - "num-traits", - "shank", - "solana-program", - "spl-associated-token-account", - "spl-token", - "thiserror", -] - -[[package]] -name = "mpl-token-metadata-context-derive" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12989bc45715b0ee91944855130131479f9c772e198a910c3eb0ea327d5bffc3" -dependencies = [ - "quote 1.0.26", - "syn 1.0.109", -] - -[[package]] -name = "mpl-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822133b6cba8f9a43e5e0e189813be63dd795858f54155c729833be472ffdb51" -dependencies = [ - "arrayref", - "borsh", - "solana-program", - "spl-token", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -2833,30 +2607,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "libc", - "memoffset 0.6.5", - "pin-utils", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2894,11 +2644,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2915,30 +2664,29 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -2959,42 +2707,42 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -3012,20 +2760,11 @@ dependencies = [ "memchr", ] -[[package]] -name = "oid-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" -dependencies = [ - "asn1-rs", -] - [[package]] name = "once_cell" -version = "1.17.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -3039,7 +2778,7 @@ version = "0.10.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -3054,9 +2793,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -3077,12 +2816,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - [[package]] name = "overload" version = "0.1.1" @@ -3101,15 +2834,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.5.3", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.52.6", ] [[package]] @@ -3118,38 +2851,20 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" -[[package]] -name = "pbkdf2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" -dependencies = [ - "crypto-mac", -] - [[package]] name = "pbkdf2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", -] - -[[package]] -name = "pem" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" -dependencies = [ - "base64 0.13.1", + "digest 0.10.7", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "percentage" @@ -3160,16 +2875,6 @@ dependencies = [ "num", ] -[[package]] -name = "pest" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" -dependencies = [ - "thiserror", - "ucd-trie", -] - [[package]] name = "pin-project" version = "1.0.12" @@ -3185,16 +2890,16 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -3202,17 +2907,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs8" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" -dependencies = [ - "der", - "spki", - "zeroize", -] - [[package]] name = "pkg-config" version = "0.3.26" @@ -3227,9 +2921,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "polyval" -version = "0.5.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", @@ -3239,9 +2933,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "0.3.19" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "ppv-lite86" @@ -3279,7 +2973,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.8", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.20", ] [[package]] @@ -3289,8 +2992,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", "version_check", ] @@ -3301,27 +3004,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "version_check", ] [[package]] name = "proc-macro2" -version = "0.4.30" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ - "unicode-xid 0.1.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" -dependencies = [ - "unicode-ident", + "unicode-ident", ] [[package]] @@ -3345,8 +3039,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -3359,72 +3053,13 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "quinn" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445cbfe2382fa023c4f2f3c7e1c95c03dcc1df2bf23cebcb2b13e1402c4394d1" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "thiserror", - "tokio", - "tracing", - "webpki", -] - -[[package]] -name = "quinn-proto" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c10f662eee9c94ddd7135043e544f3c82fa839a1e7b865911331961b53186c" -dependencies = [ - "bytes", - "rand 0.8.5", - "ring", - "rustc-hash", - "rustls", - "rustls-native-certs", - "slab", - "thiserror", - "tinyvec", - "tracing", - "webpki", -] - -[[package]] -name = "quinn-udp" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4" -dependencies = [ - "libc", - "quinn-proto", - "socket2", - "tracing", - "windows-sys 0.42.0", -] - -[[package]] -name = "quote" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" -dependencies = [ - "proc-macro2 0.4.30", -] - [[package]] name = "quote" -version = "1.0.26" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro2 1.0.56", + "proc-macro2", ] [[package]] @@ -3433,7 +3068,7 @@ version = "0.2.6" dependencies = [ "anyhow", "async-trait", - "borsh", + "borsh 1.5.1", "serde", "serde_json", "thiserror", @@ -3444,8 +3079,8 @@ name = "race-cli" version = "0.2.6" dependencies = [ "anyhow", - "base64 0.21.0", - "clap 4.2.5", + "base64 0.21.7", + "clap", "jsonrpsee", "prettytable-rs", "race-core", @@ -3474,10 +3109,13 @@ version = "0.2.6" dependencies = [ "anyhow", "async-trait", - "borsh", + "borsh 1.5.1", + "futures", "race-api", + "rs_merkle", "serde", "serde_json", + "sha256", "thiserror", ] @@ -3485,17 +3123,17 @@ dependencies = [ name = "race-encryptor" version = "0.2.6" dependencies = [ - "aes 0.8.2", + "aes", "anyhow", "arrayref", - "base64 0.21.0", + "base64 0.21.7", "chacha20", "chrono", "openssl", "race-api", "race-core", "rand 0.8.5", - "sha1", + "sha2 0.10.8", "thiserror", ] @@ -3515,7 +3153,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arrayref", - "borsh", + "borsh 1.5.1", "race-api", "race-proc-macro", "race-test", @@ -3525,7 +3163,7 @@ dependencies = [ name = "race-example-minimal" version = "0.1.0" dependencies = [ - "borsh", + "borsh 1.5.1", "race-api", "race-proc-macro", "race-test", @@ -3535,17 +3173,7 @@ dependencies = [ name = "race-example-raffle" version = "0.1.0" dependencies = [ - "borsh", - "race-api", - "race-proc-macro", - "race-test", -] - -[[package]] -name = "race-example-simple-settle" -version = "0.1.0" -dependencies = [ - "borsh", + "borsh 1.5.1", "race-api", "race-proc-macro", "race-test", @@ -3557,9 +3185,9 @@ version = "0.2.6" dependencies = [ "anyhow", "async-trait", - "base64 0.21.0", - "borsh", - "clap 4.2.5", + "base64 0.21.7", + "borsh 1.5.1", + "clap", "futures", "getrandom 0.2.11", "hyper", @@ -3577,12 +3205,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "race-local-db" +version = "0.2.6" +dependencies = [ + "anyhow", + "async-trait", + "borsh 1.5.1", + "race-api", + "race-core", + "race-env", + "race-transport", + "rusqlite", + "serde", + "serde_json", + "sha256", + "thiserror", + "tokio", +] + [[package]] name = "race-proc-macro" version = "0.2.6" dependencies = [ - "borsh", - "quote 1.0.26", + "borsh 1.5.1", + "quote", "race-api", "syn 1.0.109", ] @@ -3592,9 +3239,9 @@ name = "race-storage" version = "0.2.6" dependencies = [ "anyhow", - "base64 0.21.0", - "borsh", - "clap 4.2.5", + "base64 0.21.7", + "borsh 1.5.1", + "clap", "infer", "jsonrpsee", "openssl", @@ -3617,9 +3264,11 @@ name = "race-test" version = "0.2.6" dependencies = [ "anyhow", + "async-stream", "async-trait", - "base64 0.21.0", - "borsh", + "base64 0.21.7", + "borsh 1.5.1", + "futures", "project-root", "race-api", "race-client", @@ -3639,9 +3288,9 @@ dependencies = [ "arrayref", "async-stream", "async-trait", - "base64 0.21.0", - "borsh", - "clap 4.2.5", + "base64 0.21.7", + "borsh 1.5.1", + "clap", "futures", "hyper", "jsonrpsee", @@ -3651,16 +3300,19 @@ dependencies = [ "race-core", "race-encryptor", "race-env", + "race-local-db", "race-test", "race-transport", "serde", "serde_json", + "sha256", "thiserror", "tokio", "tokio-stream", "tower", "tower-http", "tracing", + "tracing-appender", "tracing-subscriber", "uuid", "wasmer", @@ -3671,10 +3323,11 @@ name = "race-transport" version = "0.2.6" dependencies = [ "anyhow", + "async-stream", "async-trait", - "borsh", + "borsh 1.5.1", + "futures", "jsonrpsee", - "mpl-token-metadata", "project-root", "race-api", "race-core", @@ -3683,9 +3336,12 @@ dependencies = [ "serde", "serde_json", "shellexpand", - "solana-client", + "solana-account-decoder", + "solana-pubsub-client", + "solana-rpc-client", + "solana-rpc-client-api", "solana-sdk", - "spl-associated-token-account", + "spl-associated-token-account 5.0.0", "spl-token", "thiserror", "tokio", @@ -3763,20 +3419,11 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rayon" -version = "1.7.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -3784,44 +3431,39 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] -name = "rcgen" -version = "0.10.0" +name = "redox_syscall" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "pem", - "ring", - "time 0.3.20", - "yasna", + "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -3885,7 +3527,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76e189c2369884dce920945e2ddf79b3dff49e071a167dd1817fa9c4c00d512e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "mach", "winapi", @@ -3902,12 +3544,12 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "async-compression", - "base64 0.21.0", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -3922,6 +3564,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -3931,6 +3574,8 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", @@ -3940,10 +3585,25 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] +[[package]] +name = "reqwest-middleware" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest", + "serde", + "task-local-extensions", + "thiserror", +] + [[package]] name = "ring" version = "0.16.20" @@ -3953,12 +3613,27 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.11", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + [[package]] name = "rkyv" version = "0.7.41" @@ -3967,7 +3642,7 @@ checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" dependencies = [ "bytecheck", "hashbrown 0.12.3", - "indexmap", + "indexmap 1.9.3", "ptr_meta", "rend", "rkyv_derive", @@ -3980,52 +3655,32 @@ version = "0.7.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] [[package]] -name = "rmp" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmp-serde" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" -dependencies = [ - "byteorder", - "rmp", - "serde", -] - -[[package]] -name = "rpassword" -version = "7.2.0" +name = "rs_merkle" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +checksum = "3b241d2e59b74ef9e98d94c78c47623d04c8392abaf82014dfd372a16041128f" dependencies = [ - "libc", - "rtoolbox", - "winapi", + "sha2 0.10.8", ] [[package]] -name = "rtoolbox" -version = "0.0.1" +name = "rusqlite" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "libc", - "winapi", + "bitflags 2.6.0", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] @@ -4040,31 +3695,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.17", -] - -[[package]] -name = "rusticata-macros" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" -dependencies = [ - "nom", + "semver", ] [[package]] @@ -4073,7 +3710,7 @@ version = "0.37.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -4083,14 +3720,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring", + "ring 0.17.8", + "rustls-webpki 0.101.7", "sct", - "webpki", ] [[package]] @@ -4111,14 +3748,34 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6a5fc258f1c1276dfe3016516945546e2d5383911efc0fc4f1cdc5df3a4ae3" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" @@ -4162,8 +3819,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -4173,8 +3830,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -4189,7 +3846,7 @@ version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -4208,27 +3865,9 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.17" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "send_wrapper" @@ -4238,9 +3877,9 @@ checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" [[package]] name = "serde" -version = "1.0.160" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -4258,31 +3897,32 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.9" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -4316,9 +3956,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" dependencies = [ "darling 0.20.0", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -4334,17 +3974,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.6", -] - [[package]] name = "sha1" version = "0.10.5" @@ -4353,7 +3982,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -4371,13 +4000,26 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", +] + +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2 0.10.8", + "tokio", ] [[package]] @@ -4394,48 +4036,14 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c2bb1a323307527314a36bfb73f24febb08ce2b8a554bf4ffd6f51ad15198c" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] -[[package]] -name = "shank" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63e565b5e95ad88ab38f312e89444c749360641c509ef2de0093b49f55974a5" -dependencies = [ - "shank_macro", -] - -[[package]] -name = "shank_macro" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63927d22a1e8b74bda98cc6e151fcdf178b7abb0dc6c4f81e0bbf5ffe2fc4ec8" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "shank_macro_impl", - "syn 1.0.109", -] - -[[package]] -name = "shank_macro_impl" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce03403df682f80f4dc1efafa87a4d0cb89b03726d0565e6364bdca5b9a441" -dependencies = [ - "anyhow", - "proc-macro2 1.0.56", - "quote 1.0.26", - "serde", - "syn 1.0.109", -] - [[package]] name = "sharded-slab" version = "0.1.4" @@ -4445,12 +4053,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - [[package]] name = "shellexpand" version = "3.1.0" @@ -4460,6 +4062,12 @@ dependencies = [ "dirs", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -4482,14 +4090,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] -name = "sized-chunks" -version = "0.6.5" +name = "siphasher" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" -dependencies = [ - "bitmaps", - "typenum", -] +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "slab" @@ -4508,9 +4112,9 @@ checksum = "03b634d87b960ab1a38c4fe143b508576f075e7c978bfad18217645ebfdfa2ec" [[package]] name = "smallvec" -version = "1.10.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -4535,17 +4139,17 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "sha-1 0.9.8", + "sha-1", ] [[package]] name = "solana-account-decoder" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e4ac2e5e6474d17f19341df43c62b62ee1e362bae9b06bc30223252dd4a362" +checksum = "1889be2bdb77224ed1bb726e64983a9ede622da5a22e8fb0619a2ec6885e84f2" dependencies = [ "Inflector", - "base64 0.13.1", + "base64 0.22.1", "bincode", "bs58", "bv", @@ -4553,186 +4157,69 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "solana-address-lookup-table-program", "solana-config-program", "solana-sdk", "spl-token", - "spl-token-2022", + "spl-token-2022 4.0.0", + "spl-token-group-interface 0.3.0", + "spl-token-metadata-interface 0.4.0", "thiserror", - "zstd", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "solana-compute-budget" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a9d6e202f04a2f57b314ba3494a5230f9708247070393f782a754774b1fbb3" +dependencies = [ + "rustc_version", + "solana-sdk", ] [[package]] -name = "solana-address-lookup-table-program" -version = "1.15.2" +name = "solana-config-program" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baac6a0dfc38f64e5e5e178e9eeade05ef1a2c644c95062523c6bc21f19f8866" +checksum = "92bbca6f0351b8bf3de4472a6cdc4e5f94ddd8eb571230eb6c57ed42bda10553" dependencies = [ "bincode", - "bytemuck", - "log", - "num-derive", - "num-traits", - "rustc_version 0.4.0", + "chrono", "serde", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-program", + "serde_derive", "solana-program-runtime", "solana-sdk", - "thiserror", ] [[package]] -name = "solana-clap-utils" -version = "1.15.2" +name = "solana-curve25519" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f0a43f9bcf2405e50190cf3943046663caae557db9eb71a628f359e3f4f3eea" +checksum = "0aa9209adbba7662a0a6a79ab48b4d09a2cb23a3db3dd817818e1bc28da99a11" dependencies = [ - "chrono", - "clap 2.34.0", - "rpassword", - "solana-perf", - "solana-remote-wallet", - "solana-sdk", - "thiserror", - "tiny-bip39", - "uriparse", - "url", -] - -[[package]] -name = "solana-client" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe890559c3d8e29123ed0bfba47d5d714acb1db2e4a9a981c9171960ae01425" -dependencies = [ - "async-trait", - "bincode", - "enum_dispatch", - "futures", - "futures-util", - "indexmap", - "indicatif", - "log", - "quinn", - "rand 0.7.3", - "rayon", - "solana-connection-cache", - "solana-measure", - "solana-metrics", - "solana-net-utils", - "solana-pubsub-client", - "solana-quic-client", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-rpc-client-nonce-utils", - "solana-sdk", - "solana-streamer", - "solana-thin-client", - "solana-tpu-client", - "solana-udp-client", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "solana-program", "thiserror", - "tokio", -] - -[[package]] -name = "solana-config-program" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb520c573b28060cadd8ae0fa6ae116cf74dac01078bc437d8b3e3ab00efd22" -dependencies = [ - "bincode", - "chrono", - "serde", - "serde_derive", - "solana-program-runtime", - "solana-sdk", ] [[package]] -name = "solana-connection-cache" -version = "1.15.2" +name = "solana-inline-spl" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c913dfcaf847cecd8866e4aeaa440b34c8a5dae6c1c90b7a8cb3265ff9fc775" +checksum = "9fb9977e0e7769dfef30b1390289a65eb10b9553c4590eecd1fa5abbf2224c30" dependencies = [ - "async-trait", - "bincode", - "futures-util", - "indexmap", - "log", - "rand 0.7.3", - "rayon", - "solana-measure", - "solana-metrics", - "solana-net-utils", + "bytemuck", + "rustc_version", "solana-sdk", - "thiserror", - "tokio", -] - -[[package]] -name = "solana-frozen-abi" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f7051cccdf891ac2603cdd295eb651529fe2b678b6b3af60b82dec9a9b3b06" -dependencies = [ - "ahash", - "blake3", - "block-buffer 0.9.0", - "bs58", - "bv", - "byteorder", - "cc", - "either", - "generic-array", - "getrandom 0.1.16", - "hashbrown 0.12.3", - "im", - "lazy_static", - "log", - "memmap2", - "once_cell", - "rand_core 0.6.4", - "rustc_version 0.4.0", - "serde", - "serde_bytes", - "serde_derive", - "serde_json", - "sha2 0.10.6", - "solana-frozen-abi-macro", - "subtle", - "thiserror", -] - -[[package]] -name = "solana-frozen-abi-macro" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06395428329810ade1d2518a7e75d8a6f02d01fe548aabb60ff1ba6a2eaebbe5" -dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "rustc_version 0.4.0", - "syn 1.0.109", -] - -[[package]] -name = "solana-logger" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170714ca3612e4df75f57c2c14c8ab74654b3b66f668986aeed456cedcf24446" -dependencies = [ - "env_logger", - "lazy_static", - "log", ] [[package]] name = "solana-measure" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b03528be5a0fbbe4c06a4e1758d155363b51f7c782435b1eb1d4804ab124e3" +checksum = "2d08a0175bfa4cb16889feeca9d4d5bc144a91cfbe19a3746f1823600a62684b" dependencies = [ "log", "solana-sdk", @@ -4740,9 +4227,9 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5ff9cbbe50e9918576ff46b4e38d9a946c33fc442982ce7ff397a3b851922a" +checksum = "d312fb471563f31fee1578ca8c51619458bddf25f36ad10388a2a1812ee27e25" dependencies = [ "crossbeam-channel", "gethostname", @@ -4750,150 +4237,95 @@ dependencies = [ "log", "reqwest", "solana-sdk", -] - -[[package]] -name = "solana-net-utils" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f35dff5b963ec471514e89bd99c7ac43545756221c99b63c2229cf5f37ebb2" -dependencies = [ - "bincode", - "clap 3.2.25", - "crossbeam-channel", - "log", - "nix", - "rand 0.7.3", - "serde", - "serde_derive", - "socket2", - "solana-logger", - "solana-sdk", - "solana-version", - "tokio", - "url", -] - -[[package]] -name = "solana-perf" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d630964a18fb466d79c3f5e191f37083b52b584a3f596e17f4bd41a145254d" -dependencies = [ - "ahash", - "bincode", - "bv", - "caps", - "curve25519-dalek", - "dlopen", - "dlopen_derive", - "fnv", - "lazy_static", - "libc", - "log", - "nix", - "rand 0.7.3", - "rayon", - "serde", - "solana-metrics", - "solana-rayon-threadlimit", - "solana-sdk", - "solana-vote-program", + "thiserror", ] [[package]] name = "solana-program" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae9f0fa7db3a4e90fa0df2723ac8cbc042e579cf109cd0380bc5a8c88bed924" +checksum = "51237f3618428e8b9dd1e5c3d89b8f44f00dbcb88a7ad577ebdfdfac51501c61" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "array-bytes", - "base64 0.13.1", + "ark-serialize", + "base64 0.22.1", "bincode", - "bitflags", + "bitflags 2.6.0", "blake3", - "borsh", - "borsh-derive", + "borsh 0.10.3", + "borsh 1.5.1", "bs58", "bv", "bytemuck", - "cc", + "bytemuck_derive", "console_error_panic_hook", "console_log", "curve25519-dalek", "getrandom 0.2.11", - "itertools", "js-sys", "lazy_static", - "libc", "libsecp256k1", "log", - "memoffset 0.8.0", - "num-bigint 0.4.3", + "memoffset 0.9.1", + "num-bigint 0.4.6", "num-derive", "num-traits", "parking_lot", - "rand 0.7.3", - "rand_chacha 0.2.2", - "rustc_version 0.4.0", + "rand 0.8.5", + "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", - "serde_json", - "sha2 0.10.6", - "sha3 0.10.7", - "solana-frozen-abi", - "solana-frozen-abi-macro", + "sha2 0.10.8", + "sha3 0.10.8", "solana-sdk-macro", "thiserror", - "tiny-bip39", "wasm-bindgen", - "zeroize", ] [[package]] name = "solana-program-runtime" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb3250dc9a0abc87693437ae0bb3eb02603396dcf7698c06f77c33b2c0291ca" +checksum = "f8d2c1801dc17b166db40968ef1ab30f71b319edcaa69efe8c29a66258baff52" dependencies = [ - "base64 0.13.1", + "base64 0.22.1", "bincode", "eager", - "enum-iterator 1.4.0", - "itertools", + "enum-iterator 1.5.0", + "itertools 0.12.1", "libc", - "libloading", "log", "num-derive", "num-traits", - "rand 0.7.3", - "rustc_version 0.4.0", + "percentage", + "rand 0.8.5", + "rustc_version", "serde", - "solana-frozen-abi", - "solana-frozen-abi-macro", + "solana-compute-budget", "solana-measure", "solana-metrics", "solana-sdk", + "solana-type-overrides", + "solana-vote", "solana_rbpf", "thiserror", ] [[package]] name = "solana-pubsub-client" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e441892b9a00fdceebb0e7eee3226f2f5985a15d081aab1924a298f24cdadb2" +checksum = "dfee83758014a114d622926172dc5ba661490bee32b1b6cac5b2fe06d53f3e6e" dependencies = [ "crossbeam-channel", "futures-util", "log", "reqwest", - "semver 1.0.17", + "semver", "serde", "serde_derive", "serde_json", @@ -4908,77 +4340,21 @@ dependencies = [ "url", ] -[[package]] -name = "solana-quic-client" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d19f3bd22bd8cef3bd7007e878f8ee1e9534a2b2ad99abc1ac05ed3d9f9bed" -dependencies = [ - "async-mutex", - "async-trait", - "futures", - "itertools", - "lazy_static", - "log", - "quinn", - "quinn-proto", - "quinn-udp", - "rustls", - "solana-connection-cache", - "solana-measure", - "solana-metrics", - "solana-net-utils", - "solana-rpc-client-api", - "solana-sdk", - "solana-streamer", - "solana-tpu-client", - "thiserror", - "tokio", -] - -[[package]] -name = "solana-rayon-threadlimit" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30893a53deeb0a3e32451f4f7cb063484e1504a06b127c4b40c223ea90093d7b" -dependencies = [ - "lazy_static", - "num_cpus", -] - -[[package]] -name = "solana-remote-wallet" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970f142fbf6bda164847f60977ad56adde32cafb7c798d2e005110410754aa85" -dependencies = [ - "console", - "dialoguer", - "log", - "num-derive", - "num-traits", - "parking_lot", - "qstring", - "semver 1.0.17", - "solana-sdk", - "thiserror", - "uriparse", -] - [[package]] name = "solana-rpc-client" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075485c8ce9300df10b67f01bb9e9ecb79c4c96c58e4b8aacac20e63c6144149" +checksum = "1940f4fa5dcbffc21c35ac4dc9e63545647b39f24ef92bd60ee4c14373f6c772" dependencies = [ "async-trait", - "base64 0.13.1", + "base64 0.22.1", "bincode", "bs58", "indicatif", "log", "reqwest", - "semver 1.0.17", + "reqwest-middleware", + "semver", "serde", "serde_derive", "serde_json", @@ -4993,85 +4369,70 @@ dependencies = [ [[package]] name = "solana-rpc-client-api" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0623112b87c9e65ef00538e27203b6129518d40376a4aa2ddc4fae5bf78a8a2c" +checksum = "b8045228371e1c8dd9ae7ee5d607640e245c5fb4bcaf74022ce9f7864701d187" dependencies = [ - "base64 0.13.1", + "anyhow", + "base64 0.22.1", "bs58", "jsonrpc-core", "reqwest", - "semver 1.0.17", + "reqwest-middleware", + "semver", "serde", "serde_derive", "serde_json", "solana-account-decoder", + "solana-inline-spl", "solana-sdk", "solana-transaction-status", "solana-version", - "spl-token-2022", - "thiserror", -] - -[[package]] -name = "solana-rpc-client-nonce-utils" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70673c11ff5d831c4e569b41aeb86c0e9c68dba79515b7c6f42b8f842be76fe" -dependencies = [ - "clap 2.34.0", - "solana-clap-utils", - "solana-rpc-client", - "solana-sdk", "thiserror", ] [[package]] name = "solana-sdk" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbefda9f9bda78fd9d91ae21c38d9693e94d5979838fb69b70c6addb8dab953f" +checksum = "da640d96314707ba69feb3c0b1c5fdefb418c5d5e684fe5a9a27b8daae8c18b5" dependencies = [ - "assert_matches", - "base64 0.13.1", "bincode", - "bitflags", - "borsh", + "bitflags 2.6.0", + "borsh 1.5.1", "bs58", "bytemuck", + "bytemuck_derive", "byteorder", "chrono", "derivation-path", - "digest 0.10.6", + "digest 0.10.7", "ed25519-dalek", "ed25519-dalek-bip32", "generic-array", + "getrandom 0.1.16", "hmac 0.12.1", - "itertools", + "itertools 0.12.1", "js-sys", "lazy_static", "libsecp256k1", "log", "memmap2", - "num-derive", - "num-traits", "num_enum", - "pbkdf2 0.11.0", + "pbkdf2", "qstring", "rand 0.7.3", - "rand_chacha 0.2.2", - "rustc_version 0.4.0", + "rand 0.8.5", + "rustc_version", "rustversion", "serde", "serde_bytes", "serde_derive", "serde_json", "serde_with", - "sha2 0.10.6", - "sha3 0.10.7", - "solana-frozen-abi", - "solana-frozen-abi-macro", - "solana-logger", + "sha2 0.10.8", + "sha3 0.10.8", + "siphasher", "solana-program", "solana-sdk-macro", "thiserror", @@ -5081,100 +4442,33 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f809319358d5da7c3a0ac08ebf4d87b21170d928dbb7260254e8f3061f7f9e0e" +checksum = "45b6e930455ef68cf15e5a22a0e0c207e67c3d70978a565e0a64f568c8a24129" dependencies = [ "bs58", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "solana-streamer" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd5b3dad02879b083b7218b9f9596d97cee8deda2b625bff67db95d8920f5f7" -dependencies = [ - "crossbeam-channel", - "futures-util", - "histogram", - "indexmap", - "itertools", - "libc", - "log", - "nix", - "pem", - "percentage", - "pkcs8", - "quinn", - "quinn-proto", - "quinn-udp", - "rand 0.7.3", - "rcgen", - "rustls", - "solana-metrics", - "solana-perf", - "solana-sdk", - "thiserror", - "tokio", - "x509-parser", -] - -[[package]] -name = "solana-thin-client" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bdd6347caf841a007952c748cd35c3ec8395aa3816ac59b4a9b4c102237de" -dependencies = [ - "bincode", - "log", - "rayon", - "solana-connection-cache", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-sdk", - "solana-tpu-client", + "syn 2.0.77", ] [[package]] -name = "solana-tpu-client" -version = "1.15.2" +name = "solana-security-txt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a7dfa7a85ba000656d91c8847b61f8fa9b8067443449fab8e4c35fe01dee5c" -dependencies = [ - "async-trait", - "bincode", - "futures-util", - "indexmap", - "indicatif", - "log", - "rand 0.7.3", - "rayon", - "solana-connection-cache", - "solana-measure", - "solana-metrics", - "solana-net-utils", - "solana-pubsub-client", - "solana-rpc-client", - "solana-rpc-client-api", - "solana-sdk", - "thiserror", - "tokio", -] +checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" [[package]] name = "solana-transaction-status" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b803e356fc2de0074866a6da007e721c950e754747e761a263b7f9e4c17edefa" +checksum = "4e3294eebb17ee6a2bf7f5365a332da714a8c88b92e59121fc32c495d0f7c4d8" dependencies = [ "Inflector", - "base64 0.13.1", + "base64 0.22.1", "bincode", - "borsh", + "borsh 1.5.1", "bs58", "lazy_static", "log", @@ -5182,62 +4476,68 @@ dependencies = [ "serde_derive", "serde_json", "solana-account-decoder", - "solana-address-lookup-table-program", "solana-sdk", - "spl-associated-token-account", + "spl-associated-token-account 4.0.0", "spl-memo", "spl-token", - "spl-token-2022", + "spl-token-2022 4.0.0", + "spl-token-group-interface 0.3.0", + "spl-token-metadata-interface 0.4.0", "thiserror", ] [[package]] -name = "solana-udp-client" -version = "1.15.2" +name = "solana-type-overrides" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b0d7efeb2cb7dafbf3b085c895e440b8947fe5def6bdad17ebae9badfdecb0" +checksum = "a4201087a2d944f1fe1a5531d612384a825378339b26c84df4681afb7b52018b" dependencies = [ - "async-trait", - "solana-connection-cache", - "solana-net-utils", - "solana-sdk", - "solana-streamer", - "solana-tpu-client", - "thiserror", - "tokio", + "lazy_static", + "rand 0.8.5", ] [[package]] name = "solana-version" -version = "1.15.2" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c5e80bec4215dac4a0bacd3b68be430621adf1a37aba3239032f8c91ab1517a" +dependencies = [ + "log", + "rustc_version", + "semver", + "serde", + "serde_derive", + "solana-sdk", +] + +[[package]] +name = "solana-vote" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3df7b4a4dc0a39da78d790845089a8d112fcd6d2d003ae93830387a564cfc5" +checksum = "7a72891adbea06048965dfd4e0f1897f113d03bafb9e81f895bbe560e0b3a5ec" dependencies = [ + "itertools 0.12.1", "log", - "rustc_version 0.4.0", - "semver 1.0.17", + "rustc_version", "serde", "serde_derive", - "solana-frozen-abi", - "solana-frozen-abi-macro", "solana-sdk", + "thiserror", ] [[package]] name = "solana-vote-program" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2439b8c68f000f8c3713eceabb5cabc8528d276e5bc971c694d4103d4be958ff" +checksum = "cd00cd5515020cb6c0c5167f1d173fddeae5e1f7ffd77909ea43bece86f38cc9" dependencies = [ "bincode", "log", "num-derive", "num-traits", - "rustc_version 0.4.0", + "rustc_version", "serde", "serde_derive", - "solana-frozen-abi", - "solana-frozen-abi-macro", "solana-metrics", "solana-program", "solana-program-runtime", @@ -5245,30 +4545,59 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-zk-sdk" +version = "2.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366951623087ec3221605dfaae7f94e61d7b6593fa3cc62eae1f767bcc9f7d79" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek", + "itertools 0.12.1", + "lazy_static", + "merlin", + "num-derive", + "num-traits", + "rand 0.7.3", + "serde", + "serde_derive", + "serde_json", + "sha3 0.9.1", + "solana-program", + "solana-sdk", + "subtle", + "thiserror", + "zeroize", +] + [[package]] name = "solana-zk-token-sdk" -version = "1.15.2" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a290aa32014e007b03f952d5b784433d95636c65a3fb08d19dc5658a450941c" +checksum = "860d780884e7e0eab6bdc909d80ef32b791d1c0963aff8cdfcbd4f00ca640c8a" dependencies = [ "aes-gcm-siv", - "arrayref", - "base64 0.13.1", + "base64 0.22.1", "bincode", "bytemuck", + "bytemuck_derive", "byteorder", - "cipher 0.4.4", "curve25519-dalek", - "getrandom 0.1.16", - "itertools", + "itertools 0.12.1", "lazy_static", "merlin", "num-derive", "num-traits", "rand 0.7.3", "serde", + "serde_derive", "serde_json", "sha3 0.9.1", + "solana-curve25519", "solana-program", "solana-sdk", "subtle", @@ -5278,9 +4607,9 @@ dependencies = [ [[package]] name = "solana_rbpf" -version = "0.2.38" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e9e5085099858adba23d0a0b5298da8803f89999cb567ecafab9c916cdf53d" +checksum = "ff08afd63f70a1ba712fb0017be41e93b017f7e874785b54bb5ec9aa8949781d" dependencies = [ "byteorder", "combine", @@ -5296,77 +4625,381 @@ dependencies = [ ] [[package]] -name = "spin" -version = "0.5.2" +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spl-associated-token-account" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68034596cf4804880d265f834af1ff2f821ad5293e41fa0f8f59086c181fc38e" +dependencies = [ + "assert_matches", + "borsh 1.5.1", + "num-derive", + "num-traits", + "solana-program", + "spl-token", + "spl-token-2022 4.0.0", + "thiserror", +] + +[[package]] +name = "spl-associated-token-account" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78b20903ab0fa951f07eeb4e24ea6a7f6ed91f655c2d75a5675f2ba6c73d64b" +dependencies = [ + "borsh 1.5.1", + "num-derive", + "num-traits", + "solana-program", + "spl-associated-token-account-client", + "spl-token", + "spl-token-2022 5.0.1", + "thiserror", +] + +[[package]] +name = "spl-associated-token-account-client" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5c1e2d53941ea7ebc4be3f86302a3c6c5178baa932bbc038468429e6900d36" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-discriminator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38ea8b6dedb7065887f12d62ed62c1743aa70749e8558f963609793f6fb12bc" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.77", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f05593b7ca9eac7caca309720f2eafb96355e037e6d373b909a80fe7b69b9" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.77", + "thiserror", +] + +[[package]] +name = "spl-memo" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0dba2f2bb6419523405d21c301a32c9f9568354d4742552e7972af801f4bdb3" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-pod" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c704c88fc457fa649ba3aabe195c79d885c3f26709efaddc453c8de352c90b87" +dependencies = [ + "borsh 1.5.1", + "bytemuck", + "bytemuck_derive", + "solana-program", + "solana-zk-token-sdk", + "spl-program-error", +] + +[[package]] +name = "spl-pod" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e38c99f09d58df06ca9a29fc0211786a4c34f4d099c1df27b1abaa206569a4" +dependencies = [ + "borsh 1.5.1", + "bytemuck", + "bytemuck_derive", + "solana-program", + "solana-zk-sdk", + "spl-program-error", +] + +[[package]] +name = "spl-program-error" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b28bed65356558133751cc32b48a7a5ddfc59ac4e941314630bbed1ac10532" +dependencies = [ + "num-derive", + "num-traits", + "solana-program", + "spl-program-error-derive", + "thiserror", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d375dd76c517836353e093c2dbb490938ff72821ab568b545fd30ab3256b3e" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.8", + "syn 2.0.77", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a75a5f0fcc58126693ed78a17042e9dc53f07e357d6be91789f7d62aff61a4" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod 0.3.1", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87d08a5889f14d182b19d9c4d33ee4b3402b6cbc2e057e33eda4e53f6d06029d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod 0.4.0", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-token" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a0f06ac7f23dc0984931b1fe309468f14ea58e32660439c1cef19456f5d0e3" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "thiserror", +] + +[[package]] +name = "spl-token-2022" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c10f3483e48679619c76598d4e4aebb955bc49b0a5cc63323afbf44135c9bf" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod 0.3.1", + "spl-token", + "spl-token-group-interface 0.3.0", + "spl-token-metadata-interface 0.4.0", + "spl-transfer-hook-interface 0.7.0", + "spl-type-length-value", + "thiserror", +] + +[[package]] +name = "spl-token-2022" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a3c55d87de79715d19991e25dd41787364af534cfe5721814b28d8e2453050" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive", + "num-traits", + "num_enum", + "solana-program", + "solana-security-txt", + "solana-zk-sdk", + "spl-memo", + "spl-pod 0.4.0", + "spl-token", + "spl-token-confidential-transfer-ciphertext-arithmetic", + "spl-token-confidential-transfer-proof-extraction", + "spl-token-confidential-transfer-proof-generation", + "spl-token-group-interface 0.4.1", + "spl-token-metadata-interface 0.5.0", + "spl-transfer-hook-interface 0.8.1", + "spl-type-length-value", + "thiserror", +] + +[[package]] +name = "spl-token-confidential-transfer-ciphertext-arithmetic" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48df72fb98b4069979aa4806d4a634ad6f08cb0358e732e6fbac231c5dc075bd" +dependencies = [ + "base64 0.22.1", + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-extraction" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae2ce92a0b9673c44207b21d99526b96d557d5a25752f36c38fae37c49129c3b" +dependencies = [ + "bytemuck", + "solana-curve25519", + "solana-zk-sdk", + "thiserror", +] + +[[package]] +name = "spl-token-confidential-transfer-proof-generation" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216517cc8cd26dfe768521542f221f91049be102d1eefd8054cde881d1b5d267" +dependencies = [ + "curve25519-dalek", + "solana-zk-sdk", + "thiserror", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8752b85a5ecc1d9f3a43bce3dd9a6a053673aacf5deb513d1cbb88d3534ffd" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod 0.3.1", + "spl-program-error", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "7929b448f7a36d2065ae549a80883a0cf0e94215eecf59c79240eee0cce3620f" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod 0.4.0", + "spl-program-error", +] [[package]] -name = "spki" -version = "0.5.4" +name = "spl-token-metadata-interface" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +checksum = "c6c2318ddff97e006ed9b1291ebec0750a78547f870f62a69c56fe3b46a5d8fc" dependencies = [ - "base64ct", - "der", + "borsh 1.5.1", + "solana-program", + "spl-discriminator", + "spl-pod 0.3.1", + "spl-program-error", + "spl-type-length-value", ] [[package]] -name = "spl-associated-token-account" -version = "1.1.2" +name = "spl-token-metadata-interface" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc000f0fdf1f12f99d77d398137c1751345b18c88258ce0f99b7872cf6c9bd6" +checksum = "e1cdf88690dc21db5a124bac7be5d2b6171c526a5bce157170afb1771238f049" dependencies = [ - "assert_matches", - "borsh", - "num-derive", - "num-traits", + "borsh 1.5.1", "solana-program", - "spl-token", - "spl-token-2022", - "thiserror", + "spl-discriminator", + "spl-pod 0.4.0", + "spl-program-error", + "spl-type-length-value", ] [[package]] -name = "spl-memo" -version = "3.0.1" +name = "spl-transfer-hook-interface" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0dc6f70db6bacea7ff25870b016a65ba1d1b6013536f08e4fd79a8f9005325" +checksum = "a110f33d941275d9f868b96daaa993f1e73b6806cc8836e43075b4d3ad8338a7" dependencies = [ + "arrayref", + "bytemuck", "solana-program", + "spl-discriminator", + "spl-pod 0.3.1", + "spl-program-error", + "spl-tlv-account-resolution 0.7.0", + "spl-type-length-value", ] [[package]] -name = "spl-token" -version = "3.5.0" +name = "spl-transfer-hook-interface" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e85e168a785e82564160dcb87b2a8e04cee9bfd1f4d488c729d53d6a4bd300d" +checksum = "d5864c1944c24f07c2823af91b6c578e7547d82301711506451734459a42872b" dependencies = [ "arrayref", "bytemuck", - "num-derive", - "num-traits", - "num_enum", "solana-program", - "thiserror", + "spl-discriminator", + "spl-pod 0.4.0", + "spl-program-error", + "spl-tlv-account-resolution 0.8.0", + "spl-type-length-value", ] [[package]] -name = "spl-token-2022" +name = "spl-type-length-value" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0edb869dbe159b018f17fb9bfa67118c30f232d7f54a73742bc96794dff77ed8" +checksum = "bdcd73ec187bc409464c60759232e309f83b52a18a9c5610bf281c9c6432918c" dependencies = [ - "arrayref", "bytemuck", - "num-derive", - "num-traits", - "num_enum", "solana-program", - "solana-zk-token-sdk", - "spl-memo", - "spl-token", - "thiserror", + "spl-discriminator", + "spl-pod 0.3.1", + "spl-program-error", ] [[package]] @@ -5375,12 +5008,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -5389,53 +5016,69 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "0.15.44" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 0.4.30", - "quote 0.6.13", - "unicode-xid 0.1.0", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "unicode-ident", ] [[package]] -name = "syn" -version = "2.0.15" +name = "syn_derive" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "unicode-ident", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", - "unicode-xid 0.2.4", + "core-foundation-sys", + "libc", ] [[package]] @@ -5444,6 +5087,15 @@ version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +[[package]] +name = "task-local-extensions" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" +dependencies = [ + "pin-utils", +] + [[package]] name = "tempfile" version = "3.5.0" @@ -5477,39 +5129,24 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -5522,17 +5159,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.20" @@ -5560,25 +5186,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-bip39" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" -dependencies = [ - "anyhow", - "hmac 0.8.1", - "once_cell", - "pbkdf2 0.4.0", - "rand 0.7.3", - "rustc-hash", - "sha2 0.9.9", - "thiserror", - "unicode-normalization", - "wasm-bindgen", - "zeroize", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -5596,11 +5203,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.0" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -5619,9 +5227,9 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -5636,20 +5244,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -5659,9 +5266,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.17.2" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", @@ -5669,8 +5276,7 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -5699,9 +5305,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" @@ -5709,9 +5315,20 @@ version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ - "indexmap", + "indexmap 1.9.3", + "toml_datetime", + "winnow 0.4.4", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.5.0", "toml_datetime", - "winnow", + "winnow 0.6.18", ] [[package]] @@ -5723,7 +5340,7 @@ dependencies = [ "futures-core", "futures-util", "hdrhistogram", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite", "rand 0.8.5", @@ -5737,13 +5354,13 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.3.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ "async-compression", - "base64 0.13.1", - "bitflags", + "base64 0.21.7", + "bitflags 2.6.0", "bytes", "futures-core", "futures-util", @@ -5790,15 +5407,27 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -5813,20 +5442,20 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -5848,24 +5477,23 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.17.3" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ - "base64 0.13.1", "byteorder", "bytes", + "data-encoding", "http", "httparse", "log", "rand 0.8.5", "rustls", - "sha-1 0.10.1", + "sha1", "thiserror", "url", "utf-8", - "webpki", - "webpki-roots", + "webpki-roots 0.24.0", ] [[package]] @@ -5874,12 +5502,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "unicase" version = "2.6.0" @@ -5916,25 +5538,13 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "universal-hash" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "generic-array", + "crypto-common", "subtle", ] @@ -5953,6 +5563,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "uriparse" version = "0.6.4" @@ -5965,9 +5581,9 @@ dependencies = [ [[package]] name = "url" -version = "2.3.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -6008,12 +5624,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "version_check" version = "0.9.4" @@ -6042,12 +5652,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -6056,26 +5660,27 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -6097,8 +5702,8 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5020cfa87c7cecefef118055d44e3c1fc122c7ec25701d528ee458a0b45f38f" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -6116,32 +5721,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ - "quote 1.0.26", + "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 1.0.109", + "proc-macro2", + "quote", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "wasm-encoder" @@ -6161,7 +5766,7 @@ dependencies = [ "bytes", "cfg-if", "derivative", - "indexmap", + "indexmap 1.9.3", "js-sys", "more-asserts", "serde", @@ -6229,8 +5834,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb6858f330764b1041e68d3c824970064d2fbd8e27704180289fd248ff892c48" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.56", - "quote 1.0.26", + "proc-macro2", + "quote", "syn 1.0.109", ] @@ -6243,7 +5848,7 @@ dependencies = [ "bytecheck", "enum-iterator 0.7.0", "enumset", - "indexmap", + "indexmap 1.9.3", "more-asserts", "rkyv", "target-lexicon", @@ -6264,7 +5869,7 @@ dependencies = [ "derivative", "enum-iterator 0.7.0", "fnv", - "indexmap", + "indexmap 1.9.3", "lazy_static", "libc", "mach", @@ -6283,7 +5888,7 @@ version = "0.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2ea896273ea99b15132414be1da01ab0d8836415083298ecaffbe308eaac87a" dependencies = [ - "indexmap", + "indexmap 1.9.3", "url", ] @@ -6319,24 +5924,29 @@ dependencies = [ ] [[package]] -name = "webpki" -version = "0.22.0" +name = "webpki-roots" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "ring", - "untrusted", + "rustls-webpki 0.100.3", ] [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" dependencies = [ - "webpki", + "rustls-webpki 0.101.7", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "winapi" version = "0.3.9" @@ -6423,6 +6033,15 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -6453,6 +6072,22 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6465,6 +6100,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.33.0" @@ -6483,6 +6124,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.33.0" @@ -6501,6 +6148,18 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.33.0" @@ -6519,6 +6178,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.33.0" @@ -6537,6 +6202,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -6549,6 +6220,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.33.0" @@ -6567,6 +6244,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "0.4.4" @@ -6576,40 +6259,43 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] [[package]] -name = "x509-parser" -version = "0.14.0" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "asn1-rs", - "base64 0.13.1", - "data-encoding", - "der-parser", - "lazy_static", - "nom", - "oid-registry", - "rusticata-macros", - "thiserror", - "time 0.3.20", + "zerocopy-derive", ] [[package]] -name = "yasna" -version = "0.5.2" +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ - "time 0.3.20", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -6627,9 +6313,9 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2 1.0.56", - "quote 1.0.26", - "syn 2.0.15", + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -6638,7 +6324,16 @@ version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ - "zstd-safe", + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe 7.0.0", ] [[package]] @@ -6651,6 +6346,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.8+zstd.1.5.5" diff --git a/Cargo.toml b/Cargo.toml index 152cb7e9..a7860665 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,11 @@ members = [ "env", "client", "test", - "examples/draw-card", - "examples/simple-settle", + "local-db", "examples/minimal", + "examples/draw-card", "examples/raffle", + # "examples/simple-settle", # "examples/blackjack", # "examples/roshambo", # "examples/chat", @@ -24,6 +25,7 @@ members = [ [workspace.dependencies] race-api = { path = "api", version = ">=0.2.6" } +race-local-db = { path = "local-db", version = ">=0.2.6" } race-proc-macro = { path = "proc-macro", version = ">=0.2.6" } race-core = { path = "core", version = ">=0.2.6" } race-client = { path = "client", version = ">=0.2.6" } @@ -38,13 +40,14 @@ quote = "1.0.23" anyhow = "1.0.64" tokio = "1.27.0" tokio-stream = "0.1.11" -jsonrpsee = "0.16.1" -tower-http = "0.3.4" +jsonrpsee = "0.17.1" +tower-http = "0.4.4" tower = "0.4.13" hyper = "0.14.20" tracing = "0.1.36" tracing-subscriber = "0.3.15" -borsh = "0.9.3" +tracing-appender = "0.2.3" +borsh = { version = "1.5.1", features = ["derive"] } serde_json = "1.0.85" serde = "1.0.144" thiserror = "1.0.35" @@ -56,28 +59,32 @@ async-stream = "0.3.3" async-trait = "0.1.58" futures = "0.3.25" base64 = "0.21.0" -solana-program = "1.14.18" -solana-program-test = "1.14.18" -solana-client = "1.14.18" -solana-sdk = "1.14.18" -spl-token = "3.5.0" -spl-associated-token-account = "1.1.2" -mpl-token-metadata = "1.13.0" +solana-rpc-client = "2.0.8" +solana-rpc-client-api = "2.0.8" +solana-pubsub-client = "2.0.8" +solana-account-decoder = "2.0.8" +solana-sdk = "2.0.8" +spl-token = "6.0.0" +spl-associated-token-account = "5.0.0" shellexpand = "3.0.0" getrandom = "0.2" rand = "0.8.5" toml = "0.5.9" +rusqlite = "0.32.0" project-root = "0.2.2" reqwest = "0.11.16" openssl = { version = "^0.10" } prettytable-rs = "^0.10" -sha1 = { version = "0.10.5", default-features = false, features = ["oid"] } +# sha1 = { version = "0.10.5", default-features = false, features = ["oid"] } +sha2 = "0.10.8" +sha256 = "1.5.0" aes = "0.8.2" ctr = "0.9.2" chrono = "0.4.24" chacha20 = "0.9.1" regex = "1" infer = "0.15.0" +rs_merkle = "1.4.2" [workspace.package] authors = ["RACE Foundation "] diff --git a/Justfile b/Justfile index 8026d5f2..7740630e 100644 --- a/Justfile +++ b/Justfile @@ -73,12 +73,10 @@ dev-facade *ARGS: dev-reg-transactor conf: cargo run -p race-transactor -- -c {{conf}} reg -dev-transactor conf: +dev-run-transactor conf: cargo run -p race-transactor -- -c {{conf}} run -facade-transactor num: - cargo run -p race-transactor -- -c examples/conf/server{{num}}.toml reg - cargo run -p race-transactor -- -c examples/conf/server{{num}}.toml run +dev-transactor conf: (dev-reg-transactor conf) (dev-run-transactor conf) solana: (cd contracts/solana; cargo build-sbf) diff --git a/README.md b/README.md index 4bb45e4e..bc6dfd05 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ Work in progress, not ready to accept contributions yet. | Package | Version | Description | |-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------| | @race-foundation/borsh | [![NPM](https://img.shields.io/npm/v/@race-foundation/borsh?logo=npm)](https://www.npmjs.com/package/@race-foundation/borsh) | A borsh implementation with decorators support | -| @race-fonudation/sdk-core | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-core) | SDK for RACE Protocol | -| @race-fonudation/sdk-solana | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-solana) | SDK integration for Solana blockchain | -| @race-fonudation/sdk-facade | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-facade) | SDK integration for local facade server | +| @race-foundation/sdk-core | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-core) | SDK for RACE Protocol | +| @race-foundation/sdk-solana | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-solana) | SDK integration for Solana blockchain | +| @race-foundation/sdk-facade | [![NPM](https://img.shields.io/npm/v/@race-foundation/sdk-core?logo=npm)](https://www.npmjs.com/package/@race-foundation/sdk-facade) | SDK integration for local facade server | ## Rust diff --git a/api/src/effect.rs b/api/src/effect.rs index 0f0c9433..f8ea7eb0 100644 --- a/api/src/effect.rs +++ b/api/src/effect.rs @@ -7,19 +7,21 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::{ engine::GameHandler, error::{Error, HandleError, Result}, + event::BridgeEvent, + prelude::InitAccount, random::RandomSpec, - types::{DecisionId, RandomId, Settle, Transfer}, + types::{DecisionId, GamePlayer, RandomId, Settle, Transfer}, }; #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] pub struct Ask { - pub player_addr: String, + pub player_id: u64, } #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] pub struct Assign { pub random_id: RandomId, - pub player_addr: String, + pub player_id: u64, pub indexes: Vec, } @@ -36,10 +38,74 @@ pub struct Release { #[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] pub struct ActionTimeout { - pub player_addr: String, + pub player_id: u64, pub timeout: u64, } +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +pub struct SubGame { + pub id: usize, + pub bundle_addr: String, + pub init_account: InitAccount, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +pub struct SubGameJoin { + pub id: usize, + pub players: Vec, +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +pub struct SubGameLeave { + pub id: usize, + pub player_ids: Vec, +} + +impl SubGame { + + pub fn try_new( + id: usize, + bundle_addr: String, + max_players: u16, + players: Vec, + init_data: S, + checkpoint_state: T, + ) -> Result { + Ok(Self { + id, + bundle_addr, + init_account: InitAccount { + max_players, + entry_type: crate::types::EntryType::Disabled, + players, + data: borsh::to_vec(&init_data)?, + checkpoint: Some(borsh::to_vec(&checkpoint_state)?), + }, + }) + } +} + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +pub struct EmitBridgeEvent { + pub dest: usize, + pub raw: Vec, + pub join_players: Vec, +} + +impl EmitBridgeEvent { + pub fn try_new( + dest: usize, + bridge_event: E, + join_players: Vec, + ) -> Result { + Ok(Self { + dest, + raw: borsh::to_vec(&bridge_event)?, + join_players, + }) + } +} + /// An effect used in game handler provides reading and mutating to /// the game context. An effect can be created from game context, /// manipulated by game handler and applied after event processing. @@ -69,7 +135,7 @@ pub struct ActionTimeout { /// ``` /// # use race_api::effect::Effect; /// let mut effect = Effect::default(); -/// effect.assign(1 /* random_id */, "Alice", vec![0, 1, 2] /* indexes */); +/// effect.assign(1 /* random_id */, 0 /* player_id */, vec![0, 1, 2] /* indexes */); /// ``` /// To reveal some items to the public, use [`Effect::reveal`]. /// It makes those items visible to everyone, including servers. @@ -87,7 +153,7 @@ pub struct ActionTimeout { /// ``` /// # use race_api::effect::Effect; /// let mut effect = Effect::default(); -/// let decision_id = effect.ask("Alice"); +/// let decision_id = effect.ask(0 /* player_id */); /// ``` /// /// To reveal the answer, use [`Effect::reveal_answer`]. @@ -121,15 +187,16 @@ pub struct ActionTimeout { /// use race_api::types::Settle; /// let mut effect = Effect::default(); /// // Increase assets -/// effect.settle(Settle::add("Alice", 100)); +/// effect.settle(Settle::add(0 /* player_id */, 100 /* amount */)); /// // Decrease assets -/// effect.settle(Settle::sub("Bob", 200)); +/// effect.settle(Settle::sub(1 /* player_id */, 200 /* amount */)); /// // Remove player from this game, its assets will be paid out -/// effect.settle(Settle::eject("Charlie")); +/// effect.settle(Settle::eject(2 /* player_id*/)); +/// // Make the checkpoint +/// effect.checkpoint(); /// ``` -#[cfg_attr(test, derive(PartialEq, Eq))] -#[derive(Default, BorshSerialize, BorshDeserialize, Debug)] +#[derive(Default, BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq)] pub struct Effect { pub action_timeout: Option, pub wait_timeout: Option, @@ -139,8 +206,7 @@ pub struct Effect { pub timestamp: u64, pub curr_random_id: RandomId, pub curr_decision_id: DecisionId, - pub players_count: u16, - pub servers_count: u16, + pub nodes_count: u16, pub asks: Vec, pub assigns: Vec, pub reveals: Vec, @@ -149,23 +215,38 @@ pub struct Effect { pub revealed: HashMap>, pub answered: HashMap, pub is_checkpoint: bool, - pub checkpoint: Option>, pub settles: Vec, pub handler_state: Option>, pub error: Option, pub allow_exit: bool, pub transfers: Vec, + pub launch_sub_games: Vec, + pub bridge_events: Vec, + pub valid_players: Vec, + pub is_init: bool, } impl Effect { - /// Return the number of players, including both the pending and joined. - pub fn count_players(&self) -> usize { - self.players_count as usize + fn assert_player_id(&self, id: u64, reason: &str) -> Result<()> { + if self.valid_players.is_empty() { + // Skip check + return Ok(()); + } + + if self.valid_players.iter().any(|p| p.id == id) { + Ok(()) + } else { + Err(Error::InvalidPlayerId( + id, + self.valid_players.iter().map(|p| p.id).collect(), + reason.to_string(), + )) + } } - /// Return the number of servers, including both the pending and joined. - pub fn count_servers(&self) -> usize { - self.servers_count as usize + /// Return the number of nodes, including both the pending and joined. + pub fn count_nodes(&self) -> usize { + self.nodes_count as usize } /// Initialize a random state with random spec, return random id. @@ -177,17 +258,19 @@ impl Effect { } /// Assign some random items to a specific player. - pub fn assign>( + pub fn assign( &mut self, random_id: RandomId, - player_addr: S, + player_id: u64, indexes: Vec, - ) { + ) -> Result<()> { + self.assert_player_id(player_id, "assign random id")?; self.assigns.push(Assign { random_id, - player_addr: player_addr.into(), + player_id, indexes, - }) + }); + Ok(()) } /// Reveal some random items to the public. @@ -217,13 +300,12 @@ impl Effect { } /// Ask a player for a decision, return the new decision id. - pub fn ask>(&mut self, player_addr: S) -> DecisionId { - self.asks.push(Ask { - player_addr: player_addr.into(), - }); + pub fn ask(&mut self, player_id: u64) -> Result { + self.assert_player_id(player_id, "ask decision")?; + self.asks.push(Ask { player_id }); let decision_id = self.curr_decision_id; self.curr_decision_id += 1; - decision_id + Ok(decision_id) } pub fn release(&mut self, decision_id: DecisionId) { @@ -231,11 +313,10 @@ impl Effect { } /// Dispatch action timeout event for a player after certain milliseconds. - pub fn action_timeout>(&mut self, player_addr: S, timeout: u64) { - self.action_timeout = Some(ActionTimeout { - player_addr: player_addr.into(), - timeout, - }); + pub fn action_timeout(&mut self, player_id: u64, timeout: u64) -> Result<()> { + self.assert_player_id(player_id, "set action timeout")?; + self.action_timeout = Some(ActionTimeout { player_id, timeout }); + Ok(()) } /// Return current timestamp. @@ -246,6 +327,11 @@ impl Effect { self.timestamp } + /// Cancel current dispatched event. + pub fn cancel_dispatch(&mut self) { + self.cancel_dispatch = true; + } + /// Dispatch waiting timeout event after certain milliseconds. pub fn wait_timeout(&mut self, timeout: u64) { self.wait_timeout = Some(timeout); @@ -266,21 +352,60 @@ impl Effect { self.allow_exit = allow_exit } - + /// Set current state as the checkpoint. pub fn checkpoint(&mut self) { self.is_checkpoint = true; } + pub fn is_checkpoint(&self) -> bool { + self.is_checkpoint + } + /// Submit settlements. - pub fn settle(&mut self, settle: Settle) { + /// This will set current state as checkpoint automatically. + pub fn settle(&mut self, settle: Settle) -> Result<()> { + self.checkpoint(); + self.assert_player_id(settle.id, "create settle")?; self.settles.push(settle); + Ok(()) } /// Transfer the assets to a recipient slot + /// This will set current state as checkpoint automatically. pub fn transfer(&mut self, slot_id: u8, amount: u64) { + self.checkpoint(); self.transfers.push(Transfer { slot_id, amount }); } + /// Launch sub game + pub fn launch_sub_game( + &mut self, + id: usize, + bundle_addr: String, + max_players: u16, + players: Vec, + init_data: D, + ) -> Result<()> { + for p in players.iter() { + self.assert_player_id(p.id, "launch sub game")?; + } + + self.launch_sub_games.push(SubGame { + id, + bundle_addr, + init_account: InitAccount { + max_players, + entry_type: crate::types::EntryType::Disabled, + players, + data: borsh::to_vec(&init_data)?, + // The checkpoint is always None + // It represents no checkpoint or we should use the existing one + checkpoint: None, + }, + }); + Ok(()) + } + /// Get handler state. /// /// This is an internal function, DO NOT use in game handler. @@ -295,32 +420,13 @@ impl Effect { /// /// This is an internal function, DO NOT use in game handler. pub fn __set_handler_state(&mut self, handler_state: S) { - if let Ok(state) = handler_state.try_to_vec() { + if let Ok(state) = borsh::to_vec(&handler_state) { self.handler_state = Some(state); } else { self.error = Some(HandleError::SerializationError); } } - /// Set checkpoint. - /// - /// This is an internal function, DO NOT use in game handler. - pub fn __set_checkpoint(&mut self, checkpoint_state: S) { - if let Ok(state) = checkpoint_state.try_to_vec() { - self.checkpoint = Some(state); - } else { - self.error = Some(HandleError::SerializationError); - } - } - - pub fn __set_checkpoint_raw(&mut self, raw: Vec) { - self.checkpoint = Some(raw); - } - - pub fn __checkpoint(&mut self) -> Option> { - self.checkpoint.take() - } - /// Set error. /// /// This is an internal function, DO NOT use in game handler. @@ -334,6 +440,25 @@ impl Effect { pub fn __take_error(&mut self) -> Option { self.error.take() } + + /// Emit a bridge event. + pub fn bridge_event( + &mut self, + dest: usize, + evt: E, + join_players: Vec, + ) -> Result<()> { + for p in join_players.iter() { + self.assert_player_id(p.id, "emit bridge event")?; + } + if self.bridge_events.iter().any(|x| x.dest == dest) { + return Err(Error::DuplicatedBridgeEventTarget); + } + + self.bridge_events + .push(EmitBridgeEvent::try_new(dest, evt, join_players)?); + Ok(()) + } } #[cfg(test)] @@ -341,13 +466,6 @@ mod tests { use super::*; - #[test] - fn abc() { - let data = vec![0,0,0,0,0,195,133,107,4,139,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,103,0,0,0,0,0,0,0,0,0,0,0,16,39,0,0,0,0,0,0,32,78,0,0,0,0,0,0,32,78,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6,1,0,43,0,0,0,70,97,105,108,101,100,32,116,111,32,102,105,110,100,32,97,32,112,108,97,121,101,114,32,102,111,114,32,116,104,101,32,110,101,120,116,32,98,117,116,116,111,110,1,0,0,0,0]; - let effect = Effect::try_from_slice(&data); - println!("{:?}", effect); - } - #[test] fn test_serialization() -> anyhow::Result<()> { let mut answered = HashMap::new(); @@ -361,8 +479,9 @@ mod tests { } let effect = Effect { + is_init: false, action_timeout: Some(ActionTimeout { - player_addr: "alice".into(), + player_id: 0, timeout: 100, }), wait_timeout: Some(200), @@ -372,13 +491,10 @@ mod tests { timestamp: 300_000, curr_random_id: 1, curr_decision_id: 1, - players_count: 4, - servers_count: 4, - asks: vec![Ask { - player_addr: "bob".into(), - }], + nodes_count: 4, + asks: vec![Ask { player_id: 1 }], assigns: vec![Assign { - player_addr: "bob".into(), + player_id: 1, random_id: 5, indexes: vec![0, 1, 2], }], @@ -390,15 +506,17 @@ mod tests { init_random_states: vec![RandomSpec::shuffled_list(vec!["a".into(), "b".into()])], revealed, answered, - settles: vec![Settle::add("alice", 200), Settle::sub("bob", 200)], + settles: vec![Settle::add(0, 200), Settle::sub(1, 200)], handler_state: Some(vec![1, 2, 3, 4]), error: Some(HandleError::NoEnoughPlayers), allow_exit: true, transfers: vec![], is_checkpoint: false, - checkpoint: None, + launch_sub_games: vec![], + bridge_events: vec![], + valid_players: vec![], }; - let bs = effect.try_to_vec()?; + let bs = borsh::to_vec(&effect)?; let parsed = Effect::try_from_slice(&bs)?; diff --git a/api/src/engine.rs b/api/src/engine.rs index 6b9747a2..55b6ca6e 100644 --- a/api/src/engine.rs +++ b/api/src/engine.rs @@ -1,90 +1,12 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use crate::{ - effect::Effect, - error::{HandleError, HandleResult}, - event::Event, - prelude::ServerJoin, - types::PlayerJoin, -}; - -/// A subset of on-chain account, used for game handler -/// initialization. The `access_version` may refer to an old state -/// when the game is started by transactor. -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] -pub struct InitAccount { - pub addr: String, - pub players: Vec, - pub servers: Vec, - pub data: Vec, - pub access_version: u64, - pub settle_version: u64, - pub max_players: u16, - pub checkpoint: Vec, -} - -impl InitAccount { - pub fn data(&self) -> Result { - S::try_from_slice(&self.data).or(Err(HandleError::MalformedGameAccountData)) - } - - pub fn checkpoint(&self) -> Result, HandleError> { - if self.checkpoint.is_empty() { - Ok(None) - } else { - S::try_from_slice(&self.checkpoint).or(Err(HandleError::MalformedCheckpointData)).map(Some) - } - } - - /// Add a new player. This function is only available in tests. - /// This function will panic when a duplicated position is - /// specified. - pub fn add_player>( - &mut self, - addr: S, - position: usize, - balance: u64, - verify_key: String, - ) { - self.access_version += 1; - let access_version = self.access_version; - if self.players.iter().any(|p| p.position as usize == position) { - panic!("Failed to add player, duplicated position"); - } - self.players.push(PlayerJoin { - position: position as _, - balance, - addr: addr.into(), - access_version, - verify_key, - }) - } -} - -impl Default for InitAccount { - fn default() -> Self { - Self { - addr: "".into(), - players: Vec::new(), - servers: Vec::new(), - data: Vec::new(), - access_version: 0, - settle_version: 0, - max_players: 10, - checkpoint: Vec::new(), - } - } -} +use crate::{effect::Effect, error::HandleResult, event::Event, init_account::InitAccount}; pub trait GameHandler: Sized + BorshSerialize + BorshDeserialize { - type Checkpoint: BorshSerialize + BorshDeserialize; - - /// Initialize handler state with on-chain game account data. + /// Initialize handler state with on-chain game account data. The + /// initial state must be determined by the `init_account`. fn init_state(effect: &mut Effect, init_account: InitAccount) -> HandleResult; /// Handle event. fn handle_event(&mut self, effect: &mut Effect, event: Event) -> HandleResult<()>; - - /// Create checkpoint from current state. - fn into_checkpoint(self) -> HandleResult; } diff --git a/api/src/error.rs b/api/src/error.rs index b8e50c83..20085496 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -180,6 +180,9 @@ pub enum Error { #[error("Not supported in validator mode")] NotSupportedInValidatorMode, + #[error("Not supported in sub game mode")] + NotSupportedInSubGameMode, + #[error("Invalid voter: {0}")] InvalidVoter(String), @@ -260,6 +263,51 @@ pub enum Error { #[error("Settle version mismatch, given: {0}, expected: {1}")] SettleVersionMismatch(u64, u64), + + #[error("Duplicated sub game id")] + DuplicatedSubGameId, + + #[error("Cannot map id to address: {0}")] + CantMapIdToAddr(u64), + + #[error("Cannot map address to id: {0}")] + CantMapAddrToId(String), + + #[error("Cannot bump settle version")] + CantBumpSettleVersion, + + #[error("Game uninitialized")] + GameUninitialized, + + #[error("Invalid bridge event")] + InvalidBridgeEvent, + + #[error("Invalid player id found when {2}, id: {0}, availables: {1:?}")] + InvalidPlayerId(u64, Vec, String), + + #[error("Invalid sub game id")] + InvalidSubGameId, + + #[error("Duplicated bridge event target")] + DuplicatedBridgeEventTarget, + + #[error("Missing init data")] + MissingInitData, + + #[error("Checkpoint state sha mismatch")] + CheckpointStateShaMismatch, + + #[error("Malformed checkpoint")] + MalformedCheckpoint, + + #[error("Add balance error, player: {0}, origin: {1}, amount: {2}")] + AddBalanceError(u64, u64, u64), // player_id, from_balance, to_balance + + #[error("Sub balance error, player: {0}, origin: {1}, amount: {2}")] + SubBalanceError(u64, u64, u64), // player_id, from_balance, to_balance + + #[error("Missing Checkpoint")] + MissingCheckpoint, } #[cfg(feature = "serde")] @@ -286,6 +334,9 @@ pub enum HandleError { #[error("No enough players")] NoEnoughPlayers, + #[error("No enough servers")] + NoEnoughServers, + #[error("Invalid player")] InvalidPlayer, @@ -304,11 +355,14 @@ pub enum HandleError { #[error("Malformed custom event")] MalformedCustomEvent, + #[error("Malformed bridge event")] + MalformedBridgeEvent, + #[error("Serialization error")] SerializationError, - #[error("No enough servers")] - NoEnoughServers, + #[error("Cannot initialize subgame without checkpoint")] + SubGameWithoutCheckpoint, #[error("Internal error: {message:?}")] InternalError { message: String }, diff --git a/api/src/event.rs b/api/src/event.rs index 25b0b718..e5460fb5 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -1,6 +1,6 @@ use crate::{ error::HandleError, - types::{Ciphertext, DecisionId, PlayerJoin, RandomId, SecretDigest, SecretShare, ServerJoin}, + types::{Ciphertext, DecisionId, GamePlayer, RandomId, SecretDigest, SecretShare}, }; use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] @@ -27,7 +27,7 @@ pub enum Event { /// parts is the serialized data from a custom game event which /// satisfies [`CustomEvent`]. Custom { - sender: String, + sender: u64, raw: Vec, }, @@ -38,18 +38,18 @@ pub enum Event { /// Transactor shares its secert to specific player. /// The `secret_data` is encrypted with the receiver's public key. ShareSecrets { - sender: String, + sender: u64, shares: Vec, }, OperationTimeout { - addrs: Vec, + ids: Vec, }, /// Randomize items. /// This event is sent by transactors. Mask { - sender: String, + sender: u64, random_id: RandomId, ciphertexts: Vec, }, @@ -57,7 +57,7 @@ pub enum Event { /// Lock items. /// This event is sent by transactors. Lock { - sender: String, + sender: u64, random_id: RandomId, ciphertexts_and_digests: Vec<(Ciphertext, SecretDigest)>, }, @@ -68,14 +68,9 @@ pub enum Event { random_id: RandomId, }, - /// Sync with on-chain account. New players/servers will be added frist to - /// game context and then to game handler (WASM). - /// This event is sent by transactor based on the diff of the account states. - Sync { - new_players: Vec, - new_servers: Vec, - transactor_addr: String, - access_version: u64, + /// This event is sent when new players joined game. + Join { + players: Vec, }, /// A server left the game. @@ -83,28 +78,24 @@ pub enum Event { /// /// NOTE: This event must be handled idempotently. ServerLeave { - server_addr: String, - transactor_addr: String, + server_id: u64, }, /// Client left game /// This event is sent by transactor based on client's connection status. Leave { - player_addr: String, + player_id: u64, }, /// Transactor uses this event as the start for each game. - /// The `access_version` can be used to filter out which players are included. - GameStart { - access_version: u64, - }, + GameStart, /// Timeout when waiting for start WaitingTimeout, /// Random drawer takes random items by indexes. DrawRandomItems { - sender: String, + sender: u64, random_id: usize, indexes: Vec, }, @@ -115,12 +106,12 @@ pub enum Event { /// Timeout when waiting for player's action /// Sent by transactor. ActionTimeout { - player_addr: String, + player_id: u64, }, /// Answer the decision question with encrypted ciphertext AnswerDecision { - sender: String, + sender: u64, decision_id: DecisionId, ciphertext: Ciphertext, digest: SecretDigest, @@ -133,12 +124,36 @@ pub enum Event { /// Shutdown Shutdown, + + /// The custom event from bridge + Bridge { + dest: usize, + raw: Vec, + join_players: Vec, + }, } impl std::fmt::Display for Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Event::Custom { sender, raw } => write!(f, "Custom from {}, inner: {:?}", sender, raw), + Event::Bridge { + dest, + raw, + join_players, + } => { + let players = join_players + .iter() + .map(|p| p.id.to_string()) + .collect::>() + .join(","); + + write!( + f, + "Bridge to {}, inner: [{}...], join_players: [{}]", + dest, raw[0], players + ) + } Event::Ready => write!(f, "Ready"), Event::ShareSecrets { sender, shares } => { let repr = shares @@ -157,32 +172,18 @@ impl std::fmt::Display for Event { Event::RandomnessReady { random_id } => { write!(f, "RandomnessReady, random_id: {}", random_id) } - Event::Sync { - new_players, - new_servers, - access_version, - .. - } => { - let players = new_players + Event::Join { players } => { + let players = players .iter() - .map(|p| p.addr.as_str()) - .collect::>() + .map(|p| p.id.to_string()) + .collect::>() .join(","); - let servers = new_servers - .iter() - .map(|s| s.addr.as_str()) - .collect::>() - .join(", "); - write!( - f, - "Sync, new_players: [{}], new_servers: [{}], access_version = {}", - players, servers, access_version - ) + write!(f, "Join, players: [{}]", players) } - Event::Leave { player_addr } => write!(f, "Leave from {}", player_addr), - Event::GameStart { access_version } => { - write!(f, "GameStart, access_version = {}", access_version) + Event::Leave { player_id } => write!(f, "Leave from {}", player_id), + Event::GameStart {} => { + write!(f, "GameStart") } Event::WaitingTimeout => write!(f, "WaitTimeout"), Event::DrawRandomItems { @@ -195,23 +196,16 @@ impl std::fmt::Display for Event { sender, random_id, indexes ), Event::DrawTimeout => write!(f, "DrawTimeout"), - Event::ActionTimeout { player_addr } => write!(f, "ActionTimeout for {}", player_addr), + Event::ActionTimeout { player_id } => write!(f, "ActionTimeout for {}", player_id), Event::SecretsReady { random_ids } => { write!(f, "SecretsReady for {:?}", random_ids) } - Event::ServerLeave { - server_addr, - transactor_addr, - } => write!( - f, - "ServerLeave {}, current transactor: {}", - server_addr, transactor_addr - ), + Event::ServerLeave { server_id } => write!(f, "ServerLeave {}", server_id), Event::AnswerDecision { decision_id, .. } => { write!(f, "AnswerDecision for {}", decision_id) } - Event::OperationTimeout { addrs } => { - write!(f, "OperationTimeout for {:?}", addrs) + Event::OperationTimeout { ids } => { + write!(f, "OperationTimeout for {:?}", ids) } Event::Shutdown => { write!(f, "Shutdown") @@ -221,10 +215,18 @@ impl std::fmt::Display for Event { } impl Event { - pub fn custom, E: CustomEvent>(sender: S, e: &E) -> Self { + pub fn custom(sender: u64, e: &E) -> Self { Self::Custom { - sender: sender.into(), - raw: e.try_to_vec().unwrap(), + sender, + raw: borsh::to_vec(&e).unwrap(), + } + } + + pub fn bridge(dest: usize, e: &E, join_players: Vec) -> Self { + Self::Bridge { + dest, + raw: borsh::to_vec(&e).unwrap(), + join_players, } } } @@ -234,3 +236,9 @@ pub trait CustomEvent: Sized + BorshSerialize + BorshDeserialize { Self::try_from_slice(slice).or(Err(HandleError::MalformedCustomEvent)) } } + +pub trait BridgeEvent: Sized + BorshSerialize + BorshDeserialize { + fn try_parse(slice: &[u8]) -> Result { + Self::try_from_slice(slice).or(Err(HandleError::MalformedBridgeEvent)) + } +} diff --git a/api/src/init_account.rs b/api/src/init_account.rs new file mode 100644 index 00000000..2b60910f --- /dev/null +++ b/api/src/init_account.rs @@ -0,0 +1,56 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use crate::{ + error::{HandleError, HandleResult}, + types::{EntryType, GamePlayer}, +}; + +/// A set of arguments for game handler initialization. +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +pub struct InitAccount { + pub max_players: u16, + pub entry_type: EntryType, + pub players: Vec, + pub data: Vec, + pub checkpoint: Option>, +} + +impl InitAccount { + pub fn data(&self) -> Result { + S::try_from_slice(&self.data).or(Err(HandleError::MalformedGameAccountData)) + } + + /// Add a new player. This function is only available in tests. + /// This function will panic when a duplicated position is + /// specified. + pub fn add_player(&mut self, id: u64, position: usize, balance: u64) { + if self.players.iter().any(|p| p.position as usize == position) { + panic!("Failed to add player, duplicated position"); + } + self.players.push(GamePlayer { + position: position as _, + balance, + id, + }) + } + + /// Get deserialized checkpoint, return None if not available. + pub fn checkpoint(&self) -> HandleResult> { + self.checkpoint + .as_ref() + .map(|c| T::try_from_slice(c).map_err(|_| HandleError::MalformedCheckpointData)) + .transpose() + } +} + +impl Default for InitAccount { + fn default() -> Self { + Self { + max_players: 0, + entry_type: EntryType::Disabled, + players: Vec::new(), + data: Vec::new(), + checkpoint: None, + } + } +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 9f828249..c5c80de1 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -5,6 +5,7 @@ pub mod decision; pub mod effect; +pub mod init_account; pub mod engine; pub mod error; pub mod event; diff --git a/api/src/prelude.rs b/api/src/prelude.rs index 10262eef..3552cd04 100644 --- a/api/src/prelude.rs +++ b/api/src/prelude.rs @@ -1,7 +1,8 @@ -pub use crate::effect::Effect; -pub use crate::engine::{GameHandler, InitAccount}; +pub use crate::effect::{Effect, SubGame}; +pub use crate::init_account::InitAccount; +pub use crate::engine::GameHandler; pub use crate::error::{HandleError, HandleResult}; -pub use crate::event::{CustomEvent, Event}; +pub use crate::event::{CustomEvent, Event, BridgeEvent}; pub use crate::random::{RandomStatus, RandomSpec}; -pub use crate::types::{Addr, Amount, DecisionId, PlayerJoin, RandomId, ServerJoin, Settle, GameStatus}; +pub use crate::types::{Addr, Amount, DecisionId, PlayerJoin, RandomId, ServerJoin, Settle, GameStatus, GamePlayer}; pub use borsh::{BorshDeserialize, BorshSerialize}; diff --git a/api/src/random.rs b/api/src/random.rs index 12b12f2d..7342edef 100644 --- a/api/src/random.rs +++ b/api/src/random.rs @@ -59,6 +59,9 @@ pub enum Error { #[error("No enough servers")] NoEnoughServer, + #[error("No server")] + NoServer, + #[error("Invalid reveal operation")] InvalidRevealOperation, @@ -343,7 +346,7 @@ impl RandomState { let status = if let Some(owner) = owners.first() { RandomStatus::Masking(owner.clone()) } else { - return Err(Error::NoEnoughServer); + return Err(Error::NoServer); }; Ok(Self { @@ -719,7 +722,7 @@ mod tests { #[test] fn test_mask_serialize() { let mask = Mask::new("hello"); - let encoded = mask.try_to_vec().unwrap(); + let encoded = borsh::to_vec(&mask).unwrap(); let decoded = Mask::try_from_slice(&encoded).unwrap(); assert_eq!(mask, decoded); } diff --git a/api/src/types.rs b/api/src/types.rs index e5429ff0..2f8e08b5 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -1,8 +1,8 @@ mod common; pub use common::{ - Addr, Amount, Ciphertext, DecisionId, EntryType, GameStatus, PlayerDeposit, PlayerJoin, - RandomId, RecipientSlot, RecipientSlotOwner, RecipientSlotShare, RecipientSlotType, + Addr, Amount, Ciphertext, DecisionId, EntryType, GamePlayer, GameStatus, PlayerDeposit, + PlayerJoin, RandomId, RecipientSlot, RecipientSlotOwner, RecipientSlotShare, RecipientSlotType, SecretDigest, SecretIdent, SecretKey, SecretShare, ServerJoin, Settle, SettleOp, Signature, - Transfer, VoteType, + Transfer, VoteType }; diff --git a/api/src/types/common.rs b/api/src/types/common.rs index 4da05376..49f69b3d 100644 --- a/api/src/types/common.rs +++ b/api/src/types/common.rs @@ -16,32 +16,32 @@ pub enum SettleOp { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct Settle { - pub addr: String, + pub id: u64, pub op: SettleOp, } impl Settle { - pub fn add>(addr: S, amount: u64) -> Self { + pub fn add(id: u64, amount: u64) -> Self { Self { - addr: addr.into(), + id, op: SettleOp::Add(amount), } } - pub fn sub>(addr: S, amount: u64) -> Self { + pub fn sub(id: u64, amount: u64) -> Self { Self { - addr: addr.into(), + id, op: SettleOp::Sub(amount), } } - pub fn eject>(addr: S) -> Self { + pub fn eject(id: u64) -> Self { Self { - addr: addr.into(), + id, op: SettleOp::Eject, } } - pub fn assign>(addr: S, identifier: String) -> Self { + pub fn assign(id: u64, identifier: String) -> Self { Self { - addr: addr.into(), + id, op: SettleOp::AssignSlot(identifier), } } @@ -242,13 +242,12 @@ pub enum EntryType { Cash { min_deposit: u64, max_deposit: u64 }, /// A player can join the game by pay a ticket. #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] - Ticket { - slot_id: u8, - amount: u64, - }, + Ticket { slot_id: u8, amount: u64 }, /// A player can join the game by showing a gate NFT #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] Gating { collection: String }, + #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] + Disabled, } impl Default for EntryType { @@ -347,7 +346,7 @@ impl ServerJoin { #[derive(Debug, Default, BorshSerialize, BorshDeserialize, PartialEq, Eq, Copy, Clone)] pub enum GameStatus { #[default] - Uninit, + Idle, Running, Closed, } @@ -355,9 +354,38 @@ pub enum GameStatus { impl std::fmt::Display for GameStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - GameStatus::Uninit => write!(f, "uninit"), + GameStatus::Idle => write!(f, "idle"), GameStatus::Running => write!(f, "running"), GameStatus::Closed => write!(f, "closed"), } } } + +#[derive(BorshSerialize, BorshDeserialize, Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct GamePlayer { + pub id: u64, + pub position: u16, + pub balance: u64, +} + +impl GamePlayer { + pub fn new(id: u64, balance: u64, position: u16) -> Self { + Self { + id, + position, + balance, + } + } +} + +impl From for GamePlayer { + fn from(value: PlayerJoin) -> Self { + Self { + id: value.access_version, + position: value.position, + balance: value.balance, + } + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index 203e6b27..63e6f1d0 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -9,10 +9,17 @@ use race_core::{ }, }; use race_env::{default_keyfile, parse_with_default_rpc}; -use race_storage::{arweave::Arweave, metadata::{make_metadata, MetadataT}}; +use race_storage::{ + arweave::Arweave, + metadata::{make_metadata, MetadataT}, +}; use race_transport::TransportBuilder; use serde::{Deserialize, Serialize}; -use std::{fs::{File, self}, path::PathBuf, sync::Arc}; +use tracing::level_filters::LevelFilter; +use std::{ + fs::{self, File}, path::PathBuf, sync::Arc +}; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -66,6 +73,14 @@ fn cli() -> Command { .arg(arg!( "The file path to game bundle")) .arg_required_else_help(true), ) + .subcommand( + Command::new("mint-nft") + .about("Mint NFT with an Arweave URL") + .arg(arg!( "The name of game")) + .arg(arg!( "The symbol used for game metadata file")) + .arg(arg!( "The Arweave URL")) + .arg_required_else_help(true), + ) .subcommand( Command::new("bundle-info") .about("Query game bundle information") @@ -97,12 +112,25 @@ fn cli() -> Command { .arg(arg!( "The path to specification file")) .arg_required_else_help(true), ) + .subcommand( + Command::new("reg-game") + .about("Register game account") + .arg(arg!( "The address of registration account")) + .arg(arg!( "The address of game account")) + .arg_required_else_help(true), + ) .subcommand( Command::new("close-game") .about("Close game account") .arg(arg!( "The address of game account")) .arg_required_else_help(true), ) + .subcommand( + Command::new("close-all-games") + .about("Unregister and close all games for a registration") + .arg(arg!( "The address of registration account")) + .arg_required_else_help(true), + ) .subcommand( Command::new("unreg-game") .about("Unregister game account") @@ -145,6 +173,21 @@ async fn create_transport(chain: &str, rpc: &str, keyfile: Option) -> Ar Arc::from(transport) } +async fn mint_nft( + name: String, + symbol: String, + arweave_url: String, + transport: Arc, +) { + let params = PublishGameParams { + uri: arweave_url, + name, + symbol, + }; + let resp = transport.publish_game(params).await.expect("RPC error"); + println!("Address: {}", &resp); +} + async fn publish( chain: &str, name: String, @@ -156,10 +199,23 @@ async fn publish( ) { let mut arweave = Arweave::try_new(&arkey_path).expect("Creating arweave failed"); let data = fs::read(PathBuf::from(&bundle)).expect("Wasm bundle not found"); - let bundle_addr = arweave.upload_file(data, None).await.expect("Arweave uploading wasm bundle failed"); - let metadata = make_metadata(chain, name.clone(), symbol.clone(), creator_addr, bundle_addr.clone()).expect("Creating metadata failed"); + let bundle_addr = arweave + .upload_file(data, None) + .await + .expect("Arweave uploading wasm bundle failed"); + let metadata = make_metadata( + chain, + name.clone(), + symbol.clone(), + creator_addr, + bundle_addr.clone(), + ) + .expect("Creating metadata failed"); let json_meta = metadata.json_vec().expect("Jsonify metadata failed"); - let meta_addr = arweave.upload_file(json_meta, Some("application/json")).await.expect("Arweave uploading metadata failed"); + let meta_addr = arweave + .upload_file(json_meta, Some("application/json")) + .await + .expect("Arweave uploading metadata failed"); let params = PublishGameParams { uri: meta_addr, @@ -198,12 +254,16 @@ async fn bundle_info(addr: &str, transport: Arc) { } } +#[allow(unused)] fn print_hex(data: Vec) { let mut row = vec![]; for i in data { row.push(format!("{:02x}", i)); } - let rows = row.chunks(8).map(|rows| rows.join(" ")).collect::>(); + let rows = row + .chunks(8) + .map(|rows| rows.join(" ")) + .collect::>(); for row in rows { println!("{}", row) } @@ -247,8 +307,14 @@ async fn game_info(addr: &str, transport: Arc) { println!("Vote from {} to {} for {:?}", v.voter, v.votee, v.vote_type); } println!("Current transactor: {:?}", game_account.transactor_addr); - println!("Checkpoint:"); - print_hex(game_account.checkpoint); + if let Some(cp) = game_account.checkpoint_on_chain.as_ref() { + println!("Checkpoint"); + println!(" Access Version: {}", cp.access_version); + let root: String = cp.root.iter().map(|b| format!("{:02x}", b)).collect(); + println!(" Root: {}", root); + } else { + println!("Checkpoint: None"); + } } None => { println!("Game account not found"); @@ -425,9 +491,45 @@ async fn close_game(game_addr: String, transport: Arc) { } } +async fn reg_game(reg_addr: String, game_addr: String, transport: Arc) { + println!("Register game {} from registration {}", game_addr, reg_addr); + let r = transport + .register_game(RegisterGameParams { + game_addr: game_addr.to_owned(), + reg_addr: reg_addr.to_owned(), + }) + .await; + if let Err(e) = r { + println!("Failed to register game due to: {}", e.to_string()); + } else { + println!("Game registered"); + } +} + +async fn close_all_games(reg_addr: String, transport: Arc) { + println!( + "Unregister and close all games from registration {}", + reg_addr + ); + let reg = transport + .get_registration(®_addr) + .await + .expect("Failed to load registration account"); + match reg { + Some(reg) => { + for g in reg.games { + unreg_game(reg_addr.clone(), g.addr, transport.clone()).await; + } + } + None => { + println!("Registration account {} not found", reg_addr); + } + } +} + async fn unreg_game(reg_addr: String, game_addr: String, transport: Arc) { println!( - "Unregister game {} from registration {}", + "Unregister and close game {} from registration {}", game_addr, reg_addr ); let r = transport @@ -446,7 +548,16 @@ async fn unreg_game(reg_addr: String, game_addr: String, transport: Arc("chain").expect("required"); @@ -456,6 +567,7 @@ async fn main() { println!("Interact with chain: {:?}", chain); println!("RPC Endpoint: {:?}", rpc); + println!("Specified keyfile: {:?}", keyfile); match matches.subcommand() { Some(("publish", sub_matches)) => { @@ -471,7 +583,24 @@ async fn main() { symbol.to_owned(), creator.to_owned(), bundle.to_owned(), - arweave_keyfile.expect("Arweave keyfile is required").to_owned(), + arweave_keyfile + .expect("Arweave keyfile is required") + .to_owned(), + transport, + ) + .await; + } + Some(("mint-nft", sub_matches)) => { + let name = sub_matches.get_one::("NAME").expect("required"); + let symbol = sub_matches.get_one::("SYMBOL").expect("required"); + let arweave_url = sub_matches + .get_one::("ARWEAVE_URL") + .expect("required"); + let transport = create_transport(&chain, &rpc, None).await; + mint_nft( + name.to_owned(), + symbol.to_owned(), + arweave_url.to_owned(), transport, ) .await; @@ -503,12 +632,23 @@ async fn main() { let transport = create_transport(&chain, &rpc, keyfile.cloned()).await; create_game(specs, transport).await; } + Some(("reg-game", sub_matches)) => { + let reg_addr = sub_matches.get_one::("REG").expect("required"); + let game_addr = sub_matches.get_one::("GAME").expect("required"); + let transport = create_transport(&chain, &rpc, keyfile.cloned()).await; + reg_game(reg_addr.clone(), game_addr.clone(), transport).await; + } Some(("unreg-game", sub_matches)) => { let reg_addr = sub_matches.get_one::("REG").expect("required"); let game_addr = sub_matches.get_one::("GAME").expect("required"); let transport = create_transport(&chain, &rpc, keyfile.cloned()).await; unreg_game(reg_addr.clone(), game_addr.clone(), transport).await; } + Some(("close-all-games", sub_matches)) => { + let reg_addr = sub_matches.get_one::("REG").expect("required"); + let transport = create_transport(&chain, &rpc, keyfile.cloned()).await; + close_all_games(reg_addr.clone(), transport).await; + } Some(("close-game", sub_matches)) => { let game_addr = sub_matches.get_one::("GAME").expect("required"); let transport = create_transport(&chain, &rpc, keyfile.cloned()).await; diff --git a/client/src/lib.rs b/client/src/lib.rs index 7283bde5..7b69c861 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -4,7 +4,7 @@ use std::{ }; use race_api::error::{Error, Result}; -use race_api::event::{CustomEvent, Event}; +use race_api::event::Event; use race_api::random::{RandomState, RandomStatus}; use race_api::types::SecretShare; @@ -71,6 +71,7 @@ pub struct Client { pub mode: ClientMode, pub op_hist: BTreeSet, pub secret_state: SecretState, + pub id: u64, } impl Client { @@ -91,6 +92,7 @@ impl Client { transport, encryptor, connection, + id: 0, } } @@ -113,12 +115,12 @@ impl Client { .await } - pub async fn submit_custom_event(&self, custom_event: S) -> Result<()> { - let event = Event::custom(&self.game_addr, &custom_event); - self.connection - .submit_event(&self.addr, SubmitEventParams { event }) - .await - } + // pub async fn submit_custom_event(&self, custom_event: S) -> Result<()> { + // let event = Event::custom(&self.game_addr, &custom_event); + // self.connection + // .submit_event(&self.addr, SubmitEventParams { event }) + // .await + // } pub fn load_random_states(&mut self, game_context: &GameContext) -> Result<()> { for random_state in game_context.list_random_states().iter() { @@ -133,7 +135,7 @@ impl Client { pub fn answer_event(&mut self, decision_id: DecisionId, value: String) -> Result { let (ciphertext, digest) = self.secret_state.encrypt_answer(decision_id, value)?; Ok(Event::AnswerDecision { - sender: self.addr.clone(), + sender: self.id, decision_id, ciphertext, digest, @@ -169,7 +171,7 @@ impl Client { Ok(vec![]) } else { Ok(vec![Event::ShareSecrets { - sender: self.addr.clone(), + sender: self.id, shares, }]) } @@ -220,7 +222,7 @@ impl Client { Ok(None) } else { let event = Event::ShareSecrets { - sender: self.addr.clone(), + sender: self.id, shares, }; self.op_hist.extend(op_hist); @@ -251,7 +253,7 @@ impl Client { self.encryptor.shuffle(&mut masked); let event = Event::Mask { - sender: self.addr.clone(), + sender: self.id, random_id: random_state.id, ciphertexts: masked, }; @@ -287,7 +289,7 @@ impl Client { .map_err(|e| Error::RandomizationError(e.to_string()))?; let event = Event::Lock { - sender: self.addr.clone(), + sender: self.id, random_id: random_state.id, ciphertexts_and_digests: locked, }; @@ -329,6 +331,7 @@ impl Client { } pub fn handle_updated_context(&mut self, ctx: &GameContext) -> Result> { + self.id = ctx.addr_to_id(&self.addr)?; let events = match self.mode { ClientMode::Player => { self.load_random_states(ctx)?; diff --git a/core/Cargo.toml b/core/Cargo.toml index 30aa6a7b..3bc411e6 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,12 +14,15 @@ readme.workspace = true publish = true [dependencies] +sha256 = { workspace = true } race-api = { workspace = true } async-trait = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } serde_json = { workspace = true, optional = true } borsh = { workspace = true } +futures = { workspace = true } +rs_merkle = { workspace = true } [dev-dependencies] anyhow.workspace = true diff --git a/core/src/checkpoint.rs b/core/src/checkpoint.rs new file mode 100644 index 00000000..ba78aab5 --- /dev/null +++ b/core/src/checkpoint.rs @@ -0,0 +1,225 @@ +use std::{collections::HashMap, fmt::Display}; + +use borsh::{BorshDeserialize, BorshSerialize}; +use rs_merkle::{ + algorithms::Sha256, proof_serializers::ReverseHashesOrder, Hasher, + MerkleTree, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +/// Checkpoint represents the state snapshot of game. +/// It is used as a submission to the blockchain. +#[derive(Default, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct Checkpoint { + pub root: Vec, + pub access_version: u64, + pub data: HashMap, + pub proofs: HashMap>, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct CheckpointOnChain { + pub root: Vec, + pub size: usize, + pub access_version: u64, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize, Default)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct CheckpointOffChain { + pub data: HashMap, + pub proofs: HashMap>, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct VersionedData { + pub id: usize, + pub version: u64, + pub data: Vec, + pub sha: Vec, +} + +impl Display for Checkpoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = self + .data + .iter() + .map(|(id, vd)| format!("{}#{}", id, vd.version)) + .collect::>(); + write!(f, "{}", s.join(",")) + } +} + +impl Checkpoint { + pub fn new(id: usize, access_version: u64, root_version: u64, root_data: Vec) -> Self { + let sha = Sha256::hash(&root_data); + let mut ret = Self { + root: Vec::new(), + access_version, + data: HashMap::from([( + id, + VersionedData { + id, + version: root_version, + data: root_data, + sha: sha.into(), + }, + )]), + proofs: HashMap::new(), + }; + ret.update_root_and_proofs(); + ret + } + + pub fn new_from_parts(offchain_part: CheckpointOffChain, onchain_part: CheckpointOnChain) -> Self { + Self { + proofs: offchain_part.proofs, + data: offchain_part.data, + access_version: onchain_part.access_version, + root: onchain_part.root, + } + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn update_root_and_proofs(&mut self) { + if !self.data.contains_key(&0) { + return; + } + let merkle_tree = self.to_merkle_tree(); + let Some(root) = merkle_tree.root() else { + // Skip root update as this is not a master checkpoint + return; + }; + self.root = root.into(); + let mut i = 0; + while self.data.contains_key(&i) { + let proof = merkle_tree.proof(&[i]); + let proof_bs = proof.serialize::(); + self.proofs.insert(i, proof_bs); + i += 1; + } + } + + pub fn get_data(&self, id: usize) -> Option> { + self.data.get(&id).map(|d| d.data.clone()) + } + + pub fn data(&self, id: usize) -> Vec { + self.get_data(id).unwrap_or_default() + } + + /// Set the data of the checkpoint of game. + pub fn set_data(&mut self, id: usize, data: Vec) { + let sha = Sha256::hash(&data); + if let Some(old) = self.data.get_mut(&id) { + old.data = data; + old.version += 1; + old.sha = sha.into(); + } else { + self.data.insert(id, VersionedData { + id, + version: 1, // The first checkpoint starts with version = 1 + data, + sha: sha.into(), + }); + } + self.update_root_and_proofs(); + } + + pub fn set_access_version(&mut self, access_version: u64) { + self.access_version = access_version; + } + + pub fn get_version(&self, id: usize) -> u64 { + self.data.get(&id).map(|d| d.version).unwrap_or(0) + } + + pub fn get_sha(&self, id: usize) -> Option<[u8; 32]> { + self.data + .get(&id) + .map(|d| d.sha.clone().try_into().unwrap()) + } + + /// Get version from game with id zero. + pub fn version(&self) -> u64 { + self.data.get(&0).map(|d| d.version).unwrap_or(0) + } + + pub fn to_merkle_tree(&self) -> MerkleTree { + println!("Build merkle tree, current checkpoint size: {}", self.data.len()); + let mut leaves: Vec<[u8; 32]> = vec![]; + let mut i = 0; + while let Some(vd) = self.data.get(&i) { + leaves.push(vd.sha.clone().try_into().unwrap()); + i += 1; + } + MerkleTree::from_leaves(&leaves) + } + + pub fn size(&self) -> usize { + self.data.len() + } + + pub fn derive_onchain_part(&self) -> CheckpointOnChain { + CheckpointOnChain { + size: self.data.len(), + root: self.root.clone(), + access_version: self.access_version, + } + } + + pub fn derive_offchain_part(&self) -> CheckpointOffChain { + CheckpointOffChain { + proofs: self.proofs.clone(), + data: self.data.clone(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_merkle_tree() -> anyhow::Result<()> { + let mut c = Checkpoint::default(); + let d0 = vec![1]; + c.set_data(0, d0); + let d1 = vec![2]; + c.set_data(0, d1); + let d2 = vec![3]; + c.set_data(0, d2); + println!("checkpoint: {:?}", c); + let mt = c.to_merkle_tree(); + let root = mt.root().unwrap(); + let indices = vec![0]; + let proof = mt.proof(&indices); + println!("checkpoint: {:?}", c); + println!("merkle root: {:?}", root); + let leaves = vec![c.get_sha(0).unwrap()]; + println!("leaves: {:?}", leaves); + assert!(proof.verify(root, &indices, &leaves, c.size())); + Ok(()) + } + + #[test] + fn test_set_data() -> anyhow::Result<()> { + let mut c = Checkpoint::default(); + let d = vec![1]; + c.set_data(0, d); + assert_eq!(c.version(), 1); + assert_eq!(c.data.get(&0).map(|x| x.data.clone()), Some(vec![1])); + Ok(()) + } +} diff --git a/core/src/context.rs b/core/src/context.rs index ad2a0fb0..7c5f6f82 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -1,16 +1,18 @@ use std::collections::HashMap; -use crate::types::{GameAccount, SettleTransferCheckpoint}; +use crate::checkpoint::{Checkpoint, CheckpointOffChain}; +use crate::types::{ClientMode, GameAccount, SettleWithAddr, SubGameSpec}; use borsh::{BorshDeserialize, BorshSerialize}; use race_api::decision::DecisionState; -use race_api::effect::{Ask, Assign, Effect, Release, Reveal}; +use race_api::effect::{Ask, Assign, Effect, EmitBridgeEvent, Release, Reveal, SubGame}; use race_api::engine::GameHandler; use race_api::error::{Error, Result}; use race_api::event::{CustomEvent, Event}; +use race_api::prelude::InitAccount; use race_api::random::{RandomSpec, RandomState, RandomStatus}; use race_api::types::{ - Addr, Ciphertext, DecisionId, GameStatus, PlayerJoin, RandomId, SecretDigest, SecretShare, - ServerJoin, Settle, SettleOp, Transfer, + Ciphertext, DecisionId, EntryType, GamePlayer, GameStatus, RandomId, SecretDigest, SecretShare, + SettleOp, Transfer, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -38,77 +40,31 @@ impl std::fmt::Display for NodeStatus { } } -#[derive(Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq, Clone)] +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct Player { +pub struct Node { pub addr: String, - pub position: usize, + pub id: u64, + pub mode: ClientMode, pub status: NodeStatus, - pub balance: u64, -} - -impl From for Player { - fn from(new_player: PlayerJoin) -> Self { - Self { - addr: new_player.addr, - position: new_player.position as _, - status: NodeStatus::Ready, - balance: new_player.balance, - } - } } -impl Player { - pub fn new_pending(addr: String, balance: u64, position: usize, access_version: u64) -> Self { - Self { - addr, - balance, - position, - status: NodeStatus::Pending(access_version), - } - } - - pub fn new>(addr: S, balance: u64, position: usize) -> Self { +impl Node { + pub fn new_pending>(addr: S, access_version: u64, mode: ClientMode) -> Self { Self { addr: addr.into(), - status: NodeStatus::Ready, - balance, - position, - } - } -} - -#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] -pub struct Server { - pub addr: String, - pub status: NodeStatus, - pub endpoint: String, -} - -impl From for Server { - fn from(new_server: ServerJoin) -> Self { - Self { - addr: new_server.addr, - status: NodeStatus::Ready, - endpoint: new_server.endpoint, - } - } -} - -impl Server { - pub fn new_pending>(addr: S, endpoint: String, access_version: u64) -> Self { - Server { - addr: addr.into(), - endpoint, + id: access_version, + mode, status: NodeStatus::Pending(access_version), } } - pub fn new>(addr: S, endpoint: String) -> Self { - Server { + pub fn new>(addr: S, access_version: u64, mode: ClientMode) -> Self { + Self { addr: addr.into(), - endpoint, + id: access_version, + mode, status: NodeStatus::Ready, } } @@ -126,6 +82,23 @@ impl DispatchEvent { } } +/// The effects of an event, indicates what actions should be taken +/// after the event handling. +/// +/// - checkpoint: to send a settlement. +/// - launch_sub_games: to launch a list of sub games. +/// - bridge_events: to send events to sub games. +/// - start_game: to start game. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct EventEffects { + pub settles: Vec, + pub transfers: Vec, + pub checkpoint: Option, + pub launch_sub_games: Vec, + pub bridge_events: Vec, + pub start_game: bool, +} + /// The context for public data. /// /// This information is not transmitted over the network, instead it's @@ -152,20 +125,18 @@ impl DispatchEvent { /// rejected. #[derive(Clone, Debug, BorshSerialize, BorshDeserialize)] pub struct GameContext { - pub(crate) game_addr: Addr, + pub(crate) game_addr: String, + /// The id of game, always be zero in master game. + pub(crate) game_id: usize, /// Version numbers for player/server access. This number will be /// increased whenever a new player joins or a server gets attached. pub(crate) access_version: u64, /// Version number for transactor settlement. This number will be /// increased whenever a transaction is sent. pub(crate) settle_version: u64, - /// Current transactor's address - pub(crate) transactor_addr: Addr, pub(crate) status: GameStatus, - /// List of players playing in this game - pub(crate) players: Vec, - /// List of validators serving this game - pub(crate) servers: Vec, + /// List of nodes serving this game + pub(crate) nodes: Vec, pub(crate) dispatch: Option, pub(crate) handler_state: Vec, pub(crate) timestamp: u64, @@ -175,55 +146,145 @@ pub struct GameContext { pub(crate) random_states: Vec, /// All runtime decision states, each stores the answer. pub(crate) decision_states: Vec, - /// Settles, if is not None, will be handled by event loop. - pub(crate) settles: Option>, - /// Transfers, if is not None, will be handled by event loop. - pub(crate) transfers: Option>, - /// The latest checkpoint state - pub(crate) checkpoint: Option>, + /// The checkpoint of current game + /// For master game, it contains all subgames' checkpoint + pub(crate) checkpoint: Checkpoint, + /// The sub games to launch + pub(crate) sub_games: Vec, + /// Init data from `InitAccount`. + pub(crate) init_data: Vec, + /// Maximum number of players. + pub(crate) max_players: u16, + /// Game Players + pub(crate) players: Vec, + pub(crate) entry_type: EntryType, } impl GameContext { - pub fn try_new(game_account: &GameAccount) -> Result { + pub fn try_new_with_sub_game_spec(spec: &SubGameSpec) -> Result { + let SubGameSpec { + game_addr, + nodes, + game_id, + init_account, + settle_version, + access_version, + .. + } = spec; + + Ok(Self { + game_addr: format!("{}:{}", game_addr, game_id), + game_id: *game_id, + nodes: nodes.clone(), + settle_version: *settle_version, + access_version: *access_version, + max_players: init_account.max_players, + players: init_account.players.clone(), + init_data: init_account.data.clone(), + entry_type: EntryType::Disabled, + allow_exit: false, + ..Default::default() + }) + } + + pub fn try_new( + game_account: &GameAccount, + checkpoint_off_chain: Option, + ) -> Result { let transactor_addr = game_account .transactor_addr .as_ref() .ok_or(Error::GameNotServed)?; - let players = game_account - .players + let mut nodes: Vec = game_account + .servers .iter() - .map(|p| { - Player::new_pending(p.addr.clone(), p.balance, p.position as _, p.access_version) + .map(|s| { + Node::new_pending( + s.addr.clone(), + s.access_version, + if s.addr.eq(transactor_addr) { + ClientMode::Transactor + } else { + ClientMode::Validator + }, + ) }) .collect(); + let mut players = vec![]; - let servers = game_account - .servers - .iter() - .map(|s| Server::new_pending(s.addr.clone(), s.endpoint.clone(), s.access_version)) - .collect(); + let access_version = game_account.checkpoint_on_chain.as_ref().map(|c| c.access_version).unwrap_or(game_account.access_version); + + game_account.players.iter().for_each(|p| { + // Only include those players joined before last checkpoint + if p.access_version <= access_version + { + players.push(GamePlayer::new(p.access_version, p.balance, p.position)); + } + + nodes.push(Node::new_pending( + p.addr.clone(), + p.access_version, + ClientMode::Player, + )) + }); + + let checkpoint = match (checkpoint_off_chain, game_account.checkpoint_on_chain.as_ref()) { + (Some(off_chain), Some(on_chain)) => Checkpoint::new_from_parts(off_chain, on_chain.clone()), + (None, None) => Checkpoint::default(), + _ => return Err(Error::MissingCheckpoint), + }; Ok(Self { game_addr: game_account.addr.clone(), + game_id: 0, access_version: game_account.access_version, settle_version: game_account.settle_version, - transactor_addr: transactor_addr.to_owned(), - status: GameStatus::Uninit, - players, - servers, + status: GameStatus::Idle, + nodes, dispatch: None, timestamp: 0, allow_exit: false, random_states: vec![], decision_states: vec![], - settles: None, - transfers: None, handler_state: "".into(), - checkpoint: None, + checkpoint, + sub_games: vec![], + init_data: game_account.data.clone(), + max_players: game_account.max_players, + players, + entry_type: game_account.entry_type.clone(), + }) + } + + pub fn init_account(&self) -> Result { + let checkpoint = self.checkpoint.get_data(self.game_id); + + Ok(InitAccount { + max_players: self.max_players, + entry_type: self.entry_type.clone(), + players: self.players.clone(), + data: self.init_data.clone(), + checkpoint, }) } + pub fn id_to_addr(&self, id: u64) -> Result { + self.nodes + .iter() + .find(|n| n.id == id) + .ok_or(Error::CantMapIdToAddr(id)) + .map(|n| n.addr.clone()) + } + + pub fn addr_to_id(&self, addr: &str) -> Result { + self.nodes + .iter() + .find(|n| n.addr.eq(addr)) + .ok_or(Error::CantMapAddrToId(addr.to_string())) + .map(|n| n.id) + } + pub fn set_timestamp(&mut self, timestamp: u64) { self.timestamp = timestamp; } @@ -247,65 +308,55 @@ impl GameContext { H::try_from_slice(&self.handler_state).unwrap() } - pub fn get_checkpoint(&self) -> Option> { - self.checkpoint.clone() + pub fn checkpoint(&self) -> &Checkpoint { + &self.checkpoint + } + + pub fn checkpoint_mut(&mut self) -> &mut Checkpoint { + &mut self.checkpoint } pub fn set_handler_state(&mut self, handler: &H) where H: GameHandler, { - self.handler_state = handler.try_to_vec().unwrap() + self.handler_state = borsh::to_vec(&handler).unwrap() } - pub fn get_servers(&self) -> &Vec { - &self.servers + pub fn get_nodes(&self) -> &[Node] { + &self.nodes } - pub fn get_game_addr(&self) -> &str { + pub fn game_addr(&self) -> &str { &self.game_addr } - pub fn get_transactor_addr(&self) -> &str { - &self.transactor_addr - } - - pub fn get_player_by_index(&self, index: usize) -> Option<&Player> { - self.players.get(index) - } - - pub fn get_player_mut_by_index(&mut self, index: usize) -> Option<&mut Player> { - self.players.get_mut(index) - } - - pub fn get_player_by_address(&self, addr: &str) -> Option<&Player> { - self.players.iter().find(|p| p.addr.eq(addr)) - } - - pub fn get_player_mut_by_address(&mut self, addr: &str) -> Option<&mut Player> { - self.players.iter_mut().find(|p| p.addr.eq(addr)) - } - - pub fn count_players(&self) -> u16 { - self.players.len() as u16 + pub fn game_id(&self) -> usize { + self.game_id } - pub fn count_servers(&self) -> u16 { - self.servers.len() as u16 + pub fn get_transactor_addr(&self) -> Result<&str> { + self.nodes + .iter() + .find(|n| n.mode == ClientMode::Transactor) + .as_ref() + .map(|n| n.addr.as_str()) + .ok_or(Error::InvalidTransactorAddress) } - pub fn gen_start_game_event(&self) -> Event { - Event::GameStart { - access_version: self.access_version, - } + pub fn count_nodes(&self) -> u16 { + self.nodes.len() as u16 } - pub fn get_server_by_address(&self, addr: &str) -> Option<&Server> { - self.servers.iter().find(|s| s.addr.eq(addr)) + pub fn get_node_by_address(&self, addr: &str) -> Option<&Node> { + self.nodes.iter().find(|n| n.addr.eq(addr)) } - pub fn get_transactor_server(&self) -> &Server { - self.get_server_by_address(&self.transactor_addr).unwrap() + pub fn get_transactor_node(&self) -> Result<&Node> { + self.nodes + .iter() + .find(|n| n.mode == ClientMode::Transactor) + .ok_or(Error::CantFindTransactor) } pub fn dispatch_event(&mut self, event: Event, timeout: u64) { @@ -323,18 +374,13 @@ impl GameContext { )); } - pub fn action_timeout(&mut self, player_addr: String, timeout: u64) { + pub fn action_timeout(&mut self, player_id: u64, timeout: u64) { self.dispatch = Some(DispatchEvent::new( - Event::ActionTimeout { player_addr }, + Event::ActionTimeout { player_id }, self.timestamp + timeout, )); } - pub fn start_game(&mut self) { - self.random_states.clear(); - self.dispatch = Some(DispatchEvent::new(self.gen_start_game_event(), 0)); - } - pub fn shutdown_game(&mut self) { self.dispatch = Some(DispatchEvent::new(Event::Shutdown, 0)); } @@ -343,30 +389,20 @@ impl GameContext { where E: CustomEvent, { - let event = Event::custom(self.transactor_addr.to_owned(), e); - self.dispatch_event(event, timeout); - } - - pub fn get_players(&self) -> &Vec { - &self.players + if let Ok(node) = self.get_transactor_node() { + let event = Event::custom(node.id, e); + self.dispatch_event(event, timeout); + } } pub fn get_timestamp(&self) -> u64 { self.timestamp } - pub fn is_checkpoint(&self) -> bool { - self.checkpoint.is_some() - } - pub fn get_status(&self) -> GameStatus { self.status } - // pub(crate) fn set_players(&mut self, players: Vec) { - // self.players = players; - // } - pub fn list_random_states(&self) -> &Vec { &self.random_states } @@ -387,11 +423,11 @@ impl GameContext { self.dispatch = None; } - pub fn get_access_version(&self) -> u64 { + pub fn access_version(&self) -> u64 { self.access_version } - pub fn get_settle_version(&self) -> u64 { + pub fn settle_version(&self) -> u64 { self.settle_version } @@ -434,14 +470,14 @@ impl GameContext { } /// Assign random item to a player - pub fn assign>( + pub fn assign( &mut self, random_id: RandomId, - player_addr: S, + player_addr: String, indexes: Vec, ) -> Result<()> { let rnd_st = self.get_random_state_mut(random_id)?; - rnd_st.assign(player_addr.into(), indexes)?; + rnd_st.assign(player_addr, indexes)?; Ok(()) } @@ -480,50 +516,19 @@ impl GameContext { /// Set player status by address. /// Using it in custom event handler is not allowed. - pub fn set_player_status(&mut self, addr: &str, status: NodeStatus) -> Result<()> { - if let Some(p) = self.players.iter_mut().find(|p| p.addr.eq(&addr)) { - p.status = status; + pub fn set_node_status(&mut self, addr: &str, status: NodeStatus) -> Result<()> { + if let Some(n) = self.nodes.iter_mut().find(|n| n.addr.eq(&addr)) { + n.status = status; } else { return Err(Error::InvalidPlayerAddress); } Ok(()) } - /// Add player to the game. - pub fn add_player(&mut self, player: &PlayerJoin) -> Result<()> { - if let Some(p) = self - .players - .iter() - .find(|p| p.addr.eq(&player.addr) || p.position == player.position as usize) - { - if p.position == player.position as usize { - Err(Error::PositionOccupied(p.position)) - } else { - Err(Error::PlayerAlreadyJoined(player.addr.clone())) - } - } else { - self.players.push(Player::new( - player.addr.clone(), - player.balance, - player.position as _, - )); - Ok(()) - } - } - - /// Add server to the game. - pub fn add_server(&mut self, server: &ServerJoin) -> Result<()> { - if self - .servers - .iter() - .any(|s| s.addr.eq(&server.addr)) - { - Err(Error::ServerAlreadyJoined(server.addr.clone())) - } else { - self.servers - .push(Server::new(server.addr.clone(), server.endpoint.clone())); - Ok(()) - } + pub fn add_node(&mut self, node_addr: String, access_version: u64, mode: ClientMode) { + self.nodes.retain(|n| n.addr.ne(&node_addr)); + self.nodes + .push(Node::new_pending(node_addr, access_version, mode)) } pub fn set_access_version(&mut self, access_version: u64) { @@ -534,21 +539,6 @@ impl GameContext { self.allow_exit = allow_exit; } - /// Remove player from the game. - pub fn remove_player(&mut self, addr: &str) -> Result<()> { - let orig_len = self.players.len(); - if self.allow_exit { - self.players.retain(|p| p.addr.ne(&addr)); - if orig_len == self.players.len() { - Err(Error::PlayerNotInGame) - } else { - Ok(()) - } - } else { - Err(Error::CantLeave) - } - } - /// Dispatch an event if there's none pub fn dispatch_safe(&mut self, event: Event, timeout: u64) { if self.dispatch.is_none() { @@ -568,11 +558,13 @@ impl GameContext { pub fn init_random_state(&mut self, spec: RandomSpec) -> Result { let random_id = self.random_states.len() + 1; let owners: Vec = self - .servers + .nodes .iter() - .filter_map(|s| { - if s.status == NodeStatus::Ready { - Some(s.addr.clone()) + .filter_map(|n| { + if n.status == NodeStatus::Ready + && matches!(n.mode, ClientMode::Transactor | ClientMode::Validator) + { + Some(n.addr.clone()) } else { None } @@ -587,7 +579,7 @@ impl GameContext { Ok(random_id) } - pub fn add_shared_secrets(&mut self, _addr: &str, shares: Vec) -> Result<()> { + pub fn add_shared_secrets(&mut self, _addr: String, shares: Vec) -> Result<()> { for share in shares.into_iter() { match share { SecretShare::Random { @@ -638,130 +630,46 @@ impl GameContext { pub fn dispatch_randomization_timeout(&mut self, random_id: RandomId) -> Result<()> { let no_dispatch = self.dispatch.is_none(); let rnd_st = self.get_random_state_mut(random_id)?; - match &rnd_st.status { + match rnd_st.status.clone() { RandomStatus::Shared => {} RandomStatus::Ready => { self.dispatch_event_instantly(Event::RandomnessReady { random_id }); } RandomStatus::Locking(ref addr) => { - let addr = addr.to_owned(); + let id = self.addr_to_id(addr)?; if no_dispatch { self.dispatch_event( - Event::OperationTimeout { addrs: vec![addr] }, + Event::OperationTimeout { ids: vec![id] }, OPERATION_TIMEOUT, ); } } RandomStatus::Masking(ref addr) => { - let addr = addr.to_owned(); + let id = self.addr_to_id(addr)?; if no_dispatch { self.dispatch_event( - Event::OperationTimeout { addrs: vec![addr] }, + Event::OperationTimeout { ids: vec![id] }, OPERATION_TIMEOUT, ); } } RandomStatus::WaitingSecrets => { if no_dispatch { - let addrs = rnd_st.list_operating_addrs(); - self.dispatch_event(Event::OperationTimeout { addrs }, OPERATION_TIMEOUT); + let ids = rnd_st + .list_operating_addrs() + .into_iter() + .map(|addr| self.addr_to_id(&addr)) + .collect::>>()?; + self.dispatch_event(Event::OperationTimeout { ids }, OPERATION_TIMEOUT); } } } Ok(()) } - pub fn settle(&mut self, settles: Vec) { - self.settles = Some(settles); - } - - pub fn transfer(&mut self, transfers: Vec) { - self.transfers = Some(transfers); - } - - pub fn get_settles(&self) -> &Option> { - &self.settles - } - - pub fn bump_settle_version(&mut self) { + pub fn bump_settle_version(&mut self) -> Result<()> { self.settle_version += 1; - } - - pub fn take_settles_and_transfers( - &mut self, - ) -> Result> { - if let Some(checkpoint) = self.get_checkpoint() { - let mut settles = None; - std::mem::swap(&mut settles, &mut self.settles); - - if let Some(settles) = settles.as_mut() { - settles.sort_by_key(|s| match s.op { - SettleOp::Add(_) => 0, - SettleOp::Sub(_) => 1, - SettleOp::Eject => 2, - SettleOp::AssignSlot(_) => 3, - }) - } - - for s in settles.as_ref().unwrap().iter() { - match s.op { - SettleOp::Eject => { - self.players.retain(|p| p.addr.ne(&s.addr)); - } - SettleOp::Add(amount) => { - let p = - self.get_player_mut_by_address(&s.addr) - .ok_or(Error::InvalidSettle(format!( - "Invalid player address: {}", - s.addr - )))?; - p.balance = - p.balance - .checked_add(amount) - .ok_or(Error::InvalidSettle(format!( - "Settle amount overflow (add): balance {}, change {}", - p.balance, amount, - )))?; - } - SettleOp::Sub(amount) => { - let p = - self.get_player_mut_by_address(&s.addr) - .ok_or(Error::InvalidSettle(format!( - "Invalid player address: {}", - s.addr - )))?; - p.balance = - p.balance - .checked_sub(amount) - .ok_or(Error::InvalidSettle(format!( - "Settle amount overflow (sub): balance {}, change {}", - p.balance, amount, - )))?; - } - SettleOp::AssignSlot(_) => {} - } - } - - let mut transfers = None; - std::mem::swap(&mut transfers, &mut self.transfers); - self.bump_settle_version(); - - Ok(Some(( - settles.unwrap_or(vec![]), - transfers.unwrap_or(vec![]), - checkpoint, - ))) - } else { - Ok(None) - } - } - - pub fn add_settle(&mut self, settle: Settle) { - if let Some(ref mut settles) = self.settles { - settles.push(settle); - } else { - self.settles = Some(vec![settle]); - } + Ok(()) } pub fn add_revealed_random( @@ -803,7 +711,7 @@ impl GameContext { Ok(&rnd_st.revealed) } - pub fn derive_effect(&self) -> Effect { + pub fn derive_effect(&self, is_init: bool) -> Effect { let revealed = self .list_random_states() .iter() @@ -812,9 +720,7 @@ impl GameContext { let answered = self .list_decision_states() .iter() - .filter_map(|st| { - st.get_revealed().map(|a| (st.id, a.to_owned())) - }) + .filter_map(|st| st.get_revealed().map(|a| (st.id, a.to_owned()))) .collect(); Effect { @@ -826,8 +732,7 @@ impl GameContext { timestamp: self.timestamp, curr_random_id: self.list_random_states().len() + 1, curr_decision_id: self.list_decision_states().len() + 1, - players_count: self.count_players(), - servers_count: self.count_servers(), + nodes_count: self.count_nodes(), asks: Vec::new(), assigns: Vec::new(), reveals: Vec::new(), @@ -836,16 +741,19 @@ impl GameContext { revealed, answered, is_checkpoint: false, - checkpoint: None, settles: Vec::new(), handler_state: Some(self.handler_state.clone()), error: None, allow_exit: self.allow_exit, transfers: Vec::new(), + launch_sub_games: Vec::new(), + bridge_events: Vec::new(), + valid_players: self.players.clone(), + is_init, } } - pub fn apply_effect(&mut self, effect: Effect) -> Result<()> { + pub fn apply_effect(&mut self, effect: Effect) -> Result { let Effect { action_timeout, wait_timeout, @@ -861,17 +769,22 @@ impl GameContext { transfers, handler_state, allow_exit, - checkpoint, + is_checkpoint, + launch_sub_games, + bridge_events, + error, + is_init, .. } = effect; // Handle dispatching if start_game { - self.start_game(); + // self.random_states.clear(); + // self.decision_states.clear(); } else if stop_game { self.shutdown_game(); } else if let Some(t) = action_timeout { - self.action_timeout(t.player_addr, t.timeout); + self.action_timeout(t.player_id, t.timeout); } else if let Some(t) = wait_timeout { self.wait_timeout(t); } else if cancel_dispatch { @@ -883,10 +796,11 @@ impl GameContext { for Assign { random_id, indexes, - player_addr, + player_id, } in assigns.into_iter() { - self.assign(random_id, player_addr, indexes)?; + let addr = self.id_to_addr(player_id)?; + self.assign(random_id, addr, indexes)?; } for Reveal { random_id, indexes } in reveals.into_iter() { @@ -897,41 +811,91 @@ impl GameContext { self.release(decision_id)?; } - for Ask { player_addr } in asks.into_iter() { - self.ask(player_addr); + for Ask { player_id } in asks.into_iter() { + let addr = self.id_to_addr(player_id)?; + self.ask(addr); } for spec in init_random_states.into_iter() { self.init_random_state(spec)?; } - if let Some(checkpoint_state) = checkpoint { - self.checkpoint = Some(checkpoint_state); - self.settle(settles); - self.transfer(transfers); - } else if (!settles.is_empty()) || (!transfers.is_empty()) { - return Err(Error::SettleWithoutCheckpoint); - } - if let Some(state) = handler_state { - self.handler_state = state; - } + self.handler_state = state.clone(); - Ok(()) - } + let mut settles1 = Vec::with_capacity(settles.len()); - pub fn set_node_ready(&mut self, access_version: u64) { - for s in self.servers.iter_mut() { - if let NodeStatus::Pending(a) = s.status { - if a <= access_version { - s.status = NodeStatus::Ready + if is_init && self.checkpoint.is_empty() { + self.checkpoint = Checkpoint::new( + self.game_id, + self.access_version, + self.settle_version, + state.clone(), + ); + } + + if is_checkpoint { + // Clear the random states + self.random_states.clear(); + self.decision_states.clear(); + + self.bump_settle_version()?; + self.checkpoint.set_data(self.game_id, state); + self.checkpoint.set_access_version(self.access_version); + + for s in settles { + match s.op { + SettleOp::Eject => { + self.remove_player(s.id)?; + } + SettleOp::Add(amount) => { + self.player_add_balance(s.id, amount)?; + } + SettleOp::Sub(amount) => { + self.player_sub_balance(s.id, amount)?; + } + _ => {} + } + let addr = self.id_to_addr(s.id)?; + settles1.push(SettleWithAddr { addr, op: s.op }); } + self.set_game_status(GameStatus::Idle); + } + + settles1.sort_by_key(|s| match s.op { + SettleOp::Add(_) => 0, + SettleOp::Sub(_) => 1, + SettleOp::Eject => 2, + SettleOp::AssignSlot(_) => 3, + }); + + // Append SubGame to context + for sub_game in launch_sub_games.iter().cloned() { + self.sub_games.push(sub_game); } + + return Ok(EventEffects { + settles: settles1, + transfers, + checkpoint: is_checkpoint.then(|| self.checkpoint.clone()), + launch_sub_games, + bridge_events, + start_game, + }); + } else if let Some(e) = error { + return Err(Error::HandleError(e)); + } else { + return Err(Error::InternalError( + "Missing both state and error".to_string(), + )); } - for p in self.players.iter_mut() { - if let NodeStatus::Pending(a) = p.status { + } + + pub fn set_node_ready(&mut self, access_version: u64) { + for n in self.nodes.iter_mut() { + if let NodeStatus::Pending(a) = n.status { if a <= access_version { - p.status = NodeStatus::Ready + n.status = NodeStatus::Ready } } } @@ -942,28 +906,68 @@ impl GameContext { return Err(Error::InvalidCheckpoint); } - self.players.retain(|p| match p.status { - NodeStatus::Pending(v) => v <= access_version, - NodeStatus::Confirming => true, - NodeStatus::Ready => true, - NodeStatus::Disconnected => true, - }); + self.access_version = access_version; - self.servers.retain(|s| match s.status { - NodeStatus::Pending(v) => v <= access_version || s.addr.eq(&self.transactor_addr), - NodeStatus::Confirming => true, - NodeStatus::Ready => true, - NodeStatus::Disconnected => true, - }); + Ok(()) + } - self.access_version = access_version; + pub fn init_data(&self) -> Vec { + self.init_data.clone() + } + + pub fn max_players(&self) -> u16 { + self.max_players + } + + pub fn players(&self) -> &[GamePlayer] { + &self.players + } + + pub fn add_player(&mut self, player: GamePlayer) { + self.players.push(player) + } + + pub fn player_sub_balance(&mut self, player_id: u64, amount: u64) -> Result<()> { + let p = self + .players + .iter_mut() + .find(|p| p.id == player_id) + .ok_or(Error::PlayerNotInGame)?; + + p.balance = p + .balance + .checked_sub(amount) + .ok_or(Error::SubBalanceError(player_id, p.balance, amount))?; Ok(()) } - pub fn prepare_for_next_event(&mut self, timestamp: u64) { - self.set_timestamp(timestamp); - self.checkpoint = None; + pub fn player_add_balance(&mut self, player_id: u64, amount: u64) -> Result<()> { + let p = self + .players + .iter_mut() + .find(|p| p.id == player_id) + .ok_or(Error::PlayerNotInGame)?; + + p.balance = p + .balance + .checked_add(amount) + .ok_or(Error::AddBalanceError(player_id, p.balance, amount))?; + + Ok(()) + } + + pub fn remove_player(&mut self, player_id: u64) -> Result<()> { + if let Some(index) = self.players.iter().position(|p| p.id == player_id) { + self.players.remove(index); + } else { + return Err(Error::PlayerNotInGame); + } + Ok(()) + } + + pub fn entry_type(&self) -> EntryType { + self.entry_type.clone() } } @@ -971,21 +975,23 @@ impl Default for GameContext { fn default() -> Self { Self { game_addr: "".into(), + game_id: 0, access_version: 0, settle_version: 0, - transactor_addr: "".into(), - status: GameStatus::Uninit, - players: Vec::new(), - servers: Vec::new(), + status: GameStatus::Idle, + nodes: Vec::new(), dispatch: None, handler_state: "".into(), timestamp: 0, allow_exit: false, random_states: Vec::new(), decision_states: Vec::new(), - settles: None, - transfers: None, - checkpoint: None, + checkpoint: Checkpoint::default(), + sub_games: Vec::new(), + init_data: Vec::new(), + max_players: 0, + players: Vec::new(), + entry_type: EntryType::Disabled, } } } diff --git a/core/src/engine.rs b/core/src/engine.rs index 8429bbd0..5f018f06 100644 --- a/core/src/engine.rs +++ b/core/src/engine.rs @@ -1,20 +1,8 @@ -use crate::{ - context::GameContext, - encryptor::EncryptorT, - types::{GameStatus, Settle}, -}; -use race_api::engine::InitAccount; -use race_api::error::{Error, HandleError}; +use crate::{context::GameContext, encryptor::EncryptorT, types::GameStatus}; +use race_api::error::Error; use race_api::event::Event; use race_api::random::RandomStatus; -pub fn general_init_state( - _context: &mut GameContext, - _init_account: &InitAccount, -) -> Result<(), HandleError> { - Ok(()) -} - /// A general function for system events handling. pub fn general_handle_event( context: &mut GameContext, @@ -23,14 +11,11 @@ pub fn general_handle_event( ) -> Result<(), Error> { // General event handling match event { - Event::Ready => { - // This is the first event, we make it a checkpoint - // context.checkpoint = true; - Ok(()) - } + Event::Ready => Ok(()), Event::ShareSecrets { sender, shares } => { - context.add_shared_secrets(sender, shares.clone())?; + let addr = context.id_to_addr(*sender)?; + context.add_shared_secrets(addr, shares.clone())?; let mut random_ids = Vec::::default(); for random_state in context.list_random_states_mut() { if random_state.status == RandomStatus::Shared { @@ -50,7 +35,8 @@ pub fn general_handle_event( ciphertext, digest, } => { - context.answer_decision(*decision_id, sender, ciphertext.clone(), digest.clone())?; + let addr = context.id_to_addr(*sender)?; + context.answer_decision(*decision_id, &addr, ciphertext.clone(), digest.clone())?; Ok(()) } @@ -59,7 +45,8 @@ pub fn general_handle_event( random_id, ciphertexts, } => { - context.randomize_and_mask(sender, *random_id, ciphertexts.clone())?; + let addr = context.id_to_addr(*sender)?; + context.randomize_and_mask(&addr, *random_id, ciphertexts.clone())?; Ok(()) } @@ -68,60 +55,43 @@ pub fn general_handle_event( random_id, ciphertexts_and_digests: ciphertexts_and_tests, } => { - context.lock(sender, *random_id, ciphertexts_and_tests.clone())?; + let addr = context.id_to_addr(*sender)?; + context.lock(&addr, *random_id, ciphertexts_and_tests.clone())?; Ok(()) } Event::RandomnessReady { .. } => Ok(()), - Event::Sync { - new_players, - new_servers, - transactor_addr: _, - access_version, - } => { - if *access_version <= context.access_version { - return Err(Error::EventIgnored); - } - for p in new_players.iter() { - context.add_player(p)?; - } - for s in new_servers.iter() { - context.add_server(s)?; + Event::Join { players } => { + for p in players { + context.add_player(p.to_owned()); } - context.access_version = *access_version; - Ok(()) } - Event::Leave { player_addr } => { + Event::Leave { .. } => { if !context.allow_exit { Err(Error::CantLeave) - } else if !context - .players - .iter() - .any(|p| p.addr.eq(player_addr)) - { - Err(Error::InvalidPlayerAddress) } else { Ok(()) } } - Event::GameStart { access_version } => { + Event::GameStart => { + // Update nodes' status based on current `access_version`. + context.set_node_ready(context.access_version()); context.set_game_status(GameStatus::Running); - context.set_node_ready(*access_version); Ok(()) } - Event::OperationTimeout { addrs: _ } => { + Event::OperationTimeout { ids: _ } => { // This event is for game handler Ok(()) } Event::WaitingTimeout => Ok(()), - Event::ActionTimeout { player_addr: _ } => { + Event::ActionTimeout { player_id: _ } => { // This event is for game handler Ok(()) } @@ -166,35 +136,23 @@ pub fn general_handle_event( Ok(()) } - _ => Ok(()), - } -} - -/// Context maintaining after event handling. -pub fn post_handle_event( - old_context: &GameContext, - new_context: &mut GameContext, -) -> Result<(), Error> { - // Find all leaving player, submit during the settlement. - // Or create a settlement for just player leaving. - let mut left_players = vec![]; - for p in old_context.players.iter() { - if new_context.get_player_by_address(&p.addr).is_none() { - left_players.push(p.addr.to_owned()); + Event::Bridge { + join_players, + .. + } => { + for p in join_players { + context.add_player(p.to_owned()); + } + Ok(()) } - } - for p in left_players.into_iter() { - new_context.add_settle(Settle::eject(p)); + _ => Ok(()), } - - Ok(()) } #[cfg(test)] mod tests { - use race_api::types::{ServerJoin, PlayerJoin}; use crate::encryptor::tests::DummyEncryptor; use super::*; @@ -203,47 +161,9 @@ mod tests { fn test_handle_game_start() -> anyhow::Result<()> { let encryptor = DummyEncryptor::default(); let mut context = GameContext::default(); - let event = Event::GameStart { access_version: 1 }; + let event = Event::GameStart; general_handle_event(&mut context, &event, &encryptor)?; assert_eq!(context.status, GameStatus::Running); Ok(()) } - - #[test] - fn test_handle_sync() -> anyhow::Result<()> { - let encryptor = DummyEncryptor::default(); - let mut context = GameContext::default(); - let event = Event::Sync { - new_players: vec![ - PlayerJoin { - addr: "alice".into(), - position: 0, - balance: 100, - access_version: 1, - verify_key: "VERIFY KEY".into(), - }, - PlayerJoin { - addr: "bob".into(), - position: 1, - balance: 100, - access_version: 1, - verify_key: "VERIFY KEY".into(), - }, - ], - new_servers: vec![ServerJoin { - addr: "foo".into(), - endpoint: "foo.endpoint".into(), - access_version: 1, - verify_key: "VERIFY KEY".into(), - }], - transactor_addr: "".into(), - access_version: 1, - }; - - general_handle_event(&mut context, &event, &encryptor)?; - - assert_eq!(context.count_players(), 2); - assert_eq!(context.count_servers(), 1); - Ok(()) - } } diff --git a/core/src/lib.rs b/core/src/lib.rs index d675f958..35c4ff3f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,7 +1,9 @@ pub mod encryptor; pub mod types; +pub mod checkpoint; pub mod context; pub mod engine; pub mod secret; pub mod connection; pub mod transport; +pub mod storage; diff --git a/core/src/storage.rs b/core/src/storage.rs new file mode 100644 index 00000000..b7833ad5 --- /dev/null +++ b/core/src/storage.rs @@ -0,0 +1,13 @@ +use async_trait::async_trait; +use race_api::error::Result; + +use crate::{checkpoint::CheckpointOffChain, types::{GetCheckpointParams, SaveCheckpointParams}}; + +#[async_trait] +pub trait StorageT: Send + Sync { + /// Upload the checkpoint to storage, return the proof. + async fn save_checkpoint(&self, params: SaveCheckpointParams) -> Result<()>; + + /// Get data by key from storage. + async fn get_checkpoint(&self, params: GetCheckpointParams) -> Result>; +} diff --git a/core/src/transport.rs b/core/src/transport.rs index acf916f1..b9946b20 100644 --- a/core/src/transport.rs +++ b/core/src/transport.rs @@ -1,3 +1,5 @@ +use std::pin::Pin; + use crate::types::{ AssignRecipientParams, CloseGameAccountParams, CreateGameAccountParams, CreatePlayerProfileParams, CreateRecipientParams, CreateRegistrationParams, DepositParams, @@ -7,6 +9,7 @@ use crate::types::{ VoteParams, }; use async_trait::async_trait; +use futures::Stream; use race_api::error::Result; #[async_trait] @@ -133,7 +136,7 @@ pub trait TransportT: Send + Sync { async fn publish_game(&self, params: PublishGameParams) -> Result; - async fn settle_game(&self, params: SettleParams) -> Result<()>; + async fn settle_game(&self, params: SettleParams) -> Result; async fn create_registration(&self, params: CreateRegistrationParams) -> Result; @@ -144,6 +147,9 @@ pub trait TransportT: Send + Sync { /// Get game account by its address. async fn get_game_account(&self, addr: &str, mode: QueryMode) -> Result>; + /// Subscribe game account by its address. + async fn subscribe_game_account<'a>(&'a self, addr: &'a str) -> Result> + Send + 'a>>>; + /// Get game bundle account by its address. async fn get_game_bundle(&self, addr: &str) -> Result>; diff --git a/core/src/types.rs b/core/src/types.rs index 24a0bed9..bcb966a6 100644 --- a/core/src/types.rs +++ b/core/src/types.rs @@ -2,8 +2,14 @@ mod transport_params; mod transactor_params; mod common; mod accounts; +mod broadcast_frame; +mod tx_state; +mod storage_params; +pub use storage_params::*; pub use transport_params::*; pub use transactor_params::*; pub use common::*; pub use accounts::*; +pub use broadcast_frame::*; +pub use tx_state::*; diff --git a/core/src/types/accounts.rs b/core/src/types/accounts.rs index 49f5fdf8..09b3433d 100644 --- a/core/src/types/accounts.rs +++ b/core/src/types/accounts.rs @@ -1,14 +1,17 @@ //! The data structures for on-chain accounts. -use borsh::{BorshDeserialize, BorshSerialize}; -use race_api::prelude::InitAccount; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -use crate::types::{PlayerJoin, PlayerDeposit, ServerJoin}; use super::{ common::{EntryType, VoteType}, RecipientSlot, }; +use crate::{ + checkpoint::{Checkpoint, CheckpointOnChain}, + types::{PlayerDeposit, PlayerJoin, ServerJoin}, +}; +use borsh::{BorshDeserialize, BorshSerialize}; +use race_api::prelude::InitAccount; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -42,7 +45,7 @@ pub struct ServerAccount { /// /// NFTs and Tokens are grouped by slots. A slot can only store one /// NFT or one kind of token. -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq, Default)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct RecipientAccount { @@ -118,7 +121,12 @@ pub struct RecipientAccount { /// /// The address to receive payment from the game. This is used for a /// complex payment or commission payment. -#[derive(Debug, Default, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] +/// +/// # Checkpoint +/// +/// The checkpoint is the state of the game when the settlement is +/// made. We only save the root of checkpoint merkle tree on chain. +#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct GameAccount { @@ -140,52 +148,45 @@ pub struct GameAccount { pub data: Vec, pub entry_type: EntryType, pub recipient_addr: String, - pub checkpoint: Vec, - pub checkpoint_access_version: u64, + pub checkpoint_on_chain: Option, } impl GameAccount { - pub fn derive_init_account(&self) -> InitAccount { - let game_account = self.to_owned(); + pub fn derive_init_account(&self, checkpoint: &Checkpoint) -> InitAccount { InitAccount { - addr: game_account.addr, - players: game_account.players.clone(), - servers: game_account.servers.clone(), - data: game_account.data.clone(), - access_version: game_account.access_version, - settle_version: game_account.settle_version, - max_players: game_account.max_players, - checkpoint: game_account.checkpoint, + max_players: self.max_players, + entry_type: self.entry_type.clone(), + players: self.players.iter().cloned().map(Into::into).collect(), + data: self.data.clone(), + checkpoint: checkpoint.get_data(0), } } - pub fn derive_rollbacked_init_account(&self) -> InitAccount { - let game_account = self.to_owned(); - let Self { players, servers, addr, data, max_players, checkpoint, checkpoint_access_version, settle_version, transactor_addr, .. } = game_account; - + pub fn derive_init_account_with_empty_checkpoint(&self) -> InitAccount { + InitAccount { + max_players: self.max_players, + entry_type: self.entry_type.clone(), + players: self.players.iter().cloned().map(Into::into).collect(), + data: self.data.clone(), + checkpoint: None, + } + } - let is_transactor = |addr: &String| { - transactor_addr.clone().is_some_and(|a| a.eq(addr)) - }; - let players = players - .into_iter() - .filter(|p| p.access_version <= checkpoint_access_version) - .collect(); - let servers = servers - .into_iter() - // There's no sync event for transactor, so here we always include transactor address - .filter(|s| s.access_version <= checkpoint_access_version || is_transactor(&s.addr)) + pub fn derive_checkpoint_init_account(&self, checkpoint: &Checkpoint) -> InitAccount { + let players = self + .players + .iter() + .filter(|p| p.access_version <= checkpoint.access_version) + .cloned() + .map(|p| p.into()) .collect(); InitAccount { - addr, + entry_type: self.entry_type.clone(), + max_players: self.max_players, players, - servers, - data, - access_version: checkpoint_access_version, - settle_version, - max_players, - checkpoint + data: self.data.clone(), + checkpoint: checkpoint.get_data(0), } } } @@ -239,250 +240,3 @@ pub struct PlayerProfile { pub nick: String, pub pfp: Option, } - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_server_account() { - let s = ServerAccount { - addr: "an addr".to_string(), - endpoint: "http://foo.bar".to_string(), - }; - - let res_bytes = [ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, - 47, 102, 111, 111, 46, 98, 97, 114, - ]; - let ser = s.try_to_vec().unwrap(); - println!("Serialized server account {:?}", ser); - assert_eq!(ser, res_bytes); - // let decoded = ServerAccount::try_from_slice(&res).unwrap(); - // assert_eq!(decoded, res); - } - - #[test] - fn test_player_profile() { - let p = PlayerProfile { - addr: "an addr".to_string(), - nick: "Alice".to_string(), - pfp: Some("Awesome PFP".to_string()), - }; - let bytes = [ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 5, 0, 0, 0, 65, 108, 105, 99, 101, 1, 11, - 0, 0, 0, 65, 119, 101, 115, 111, 109, 101, 32, 80, 70, 80, - ]; - - let ser = p.try_to_vec().unwrap(); - println!("Serialized player profile {:?}", ser); - - assert_eq!(ser, bytes); - } - - #[test] - fn test_player_profile2() { - let p = PlayerProfile { - addr: "FhgSDdsXx88htt3kzn1GToXwMtE3VTvg2iyimQUoqELe".to_string(), - nick: "Gentoo".to_string(), - pfp: None, - }; - - let p1 = PlayerProfile::try_from_slice(&[ - 44, 0, 0, 0, 70, 104, 103, 83, 68, 100, 115, 88, 120, 56, 56, 104, 116, 116, 51, 107, - 122, 110, 49, 71, 84, 111, 88, 119, 77, 116, 69, 51, 86, 84, 118, 103, 50, 105, 121, - 105, 109, 81, 85, 111, 113, 69, 76, 101, 6, 0, 0, 0, 71, 101, 110, 116, 111, 111, 0, - ]) - .unwrap(); - assert_eq!(p.addr, p1.addr); - assert_eq!(p.nick, p1.nick); - assert_eq!(p.pfp, p1.pfp); - } - - #[test] - fn test_reg() { - let reg = RegistrationAccount { - addr: "an addr".to_string(), - is_private: true, - size: 100, - owner: Some("another addr".to_string()), - games: vec![ - GameRegistration { - title: "Game A".to_string(), - addr: "addr 0".to_string(), - reg_time: 1000u64, - bundle_addr: "bundle 0".to_string(), - }, - GameRegistration { - title: "Game B".to_string(), - addr: "addr 1".to_string(), - reg_time: 1001u64, - bundle_addr: "bundle 1".to_string(), - }, - ], - }; - let bytes = [ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 1, 100, 0, 1, 12, 0, 0, 0, 97, 110, 111, - 116, 104, 101, 114, 32, 97, 100, 100, 114, 2, 0, 0, 0, 6, 0, 0, 0, 71, 97, 109, 101, - 32, 65, 6, 0, 0, 0, 97, 100, 100, 114, 32, 48, 232, 3, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, - 98, 117, 110, 100, 108, 101, 32, 48, 6, 0, 0, 0, 71, 97, 109, 101, 32, 66, 6, 0, 0, 0, - 97, 100, 100, 114, 32, 49, 233, 3, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 98, 117, 110, 100, - 108, 101, 32, 49, - ]; - - let ser = reg.try_to_vec().unwrap(); - println!("Serialized reg {:?}", ser); - - assert_eq!(ser, bytes); - } - - #[test] - fn test_game_bundle() { - let game_bunle = GameBundle { - uri: "http://foo.bar".to_string(), - name: "Awesome Game".to_string(), - data: vec![1, 2, 3, 4], - }; - - let bytes = [ - 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 12, 0, 0, - 0, 65, 119, 101, 115, 111, 109, 101, 32, 71, 97, 109, 101, 4, 0, 0, 0, 1, 2, 3, 4, - ]; - let ser = game_bunle.try_to_vec().unwrap(); - println!("Serialized game bundle {:?}", ser); - - assert_eq!(ser, bytes); - } - - #[test] - fn test_game_account() { - let game_account = GameAccount { - addr: "game addr".to_string(), - title: "awesome game title".to_string(), - bundle_addr: "bundle addr".to_string(), - token_addr: "token addr".to_string(), - owner_addr: "owner addr".to_string(), - settle_version: 10u64, - access_version: 20u64, - players: vec![ - PlayerJoin { - addr: "player 0".to_string(), - balance: 3u64, - position: 0u16, - access_version: 19u64, - verify_key: "VERIFY KEY".into(), - }, - PlayerJoin { - addr: "player 1".to_string(), - balance: 6u64, - position: 1u16, - access_version: 21u64, - verify_key: "VERIFY KEY".into(), - }, - ], - deposits: vec![ - PlayerDeposit { - addr: "player 0".to_string(), - amount: 9u64, - settle_version: 21u64, - }, - PlayerDeposit { - addr: "player 1".to_string(), - amount: 12u64, - settle_version: 21u64, - }, - ], - servers: vec![ - ServerJoin { - addr: "server 0".to_string(), - endpoint: "http://foo.bar".to_string(), - access_version: 17u64, - verify_key: "VERIFY KEY".into(), - }, - ServerJoin { - addr: "server 1".to_string(), - endpoint: "http://foo.bar".to_string(), - access_version: 17u64, - verify_key: "VERIFY KEY".into(), - }, - ], - transactor_addr: Some("server 0".to_string()), - votes: vec![Vote { - voter: "server 1".to_string(), - votee: "server 0".to_string(), - vote_type: VoteType::ServerVoteTransactorDropOff, - }], - unlock_time: None, - max_players: 30u16, - data_len: 10u32, - data: vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - recipient_addr: "recipient".to_string(), - entry_type: EntryType::Cash { - min_deposit: 100, - max_deposit: 250, - }, - checkpoint: vec![], - checkpoint_access_version: 0, - }; - let bytes = [ - 9, 0, 0, 0, 103, 97, 109, 101, 32, 97, 100, 100, 114, 18, 0, 0, 0, 97, 119, 101, 115, - 111, 109, 101, 32, 103, 97, 109, 101, 32, 116, 105, 116, 108, 101, 11, 0, 0, 0, 98, - 117, 110, 100, 108, 101, 32, 97, 100, 100, 114, 10, 0, 0, 0, 116, 111, 107, 101, 110, - 32, 97, 100, 100, 114, 10, 0, 0, 0, 111, 119, 110, 101, 114, 32, 97, 100, 100, 114, 10, - 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, - 121, 101, 114, 32, 48, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, - 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, - 49, 1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, - 70, 89, 32, 75, 69, 89, 2, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, 48, 9, - 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, - 32, 49, 12, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 115, - 101, 114, 118, 101, 114, 32, 48, 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, - 111, 46, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, 70, 89, 32, - 75, 69, 89, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 49, 14, 0, 0, 0, 104, 116, - 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 0, 10, 0, - 0, 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 1, 8, 0, 0, 0, 115, 101, 114, 118, 101, - 114, 32, 48, 1, 0, 0, 0, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 49, 8, 0, 0, 0, - 115, 101, 114, 118, 101, 114, 32, 48, 0, 0, 30, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 1, 2, - 3, 4, 5, 6, 7, 8, 9, 0, 100, 0, 0, 0, 0, 0, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, - 114, 101, 99, 105, 112, 105, 101, 110, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 - ]; - let ser = game_account.try_to_vec().unwrap(); - println!("Serialized game account {:?}", ser); - - assert_eq!(ser, bytes); - } - - #[test] - fn test_reg_account() { - let reg_account = RegistrationAccount { - addr: "DUMZU76rYgYkXuaCBB2m65USqeeCd1T1Gk2mpfgKKrS4".to_string(), - is_private: false, - size: 100, - owner: Some("J22ir2nLxVRqUrcpwMDBM47HpFCLLRrKFroF6LjK7DEA".to_string()), - games: vec![GameRegistration { - title: "My Raffle".to_string(), - addr: "DbsLWtwanc5VrGHH9FBqxTfhQA1XxCitsQCAcahade9r".to_string(), - reg_time: 1682425340u64, - bundle_addr: "96VJaPnUHuCGsvX1eACQPDU5R7WxwB2TxQY6CL947DqF".to_string(), - }], - }; - - let bytes = [ - 44, 0, 0, 0, 68, 85, 77, 90, 85, 55, 54, 114, 89, 103, 89, 107, 88, 117, 97, 67, 66, - 66, 50, 109, 54, 53, 85, 83, 113, 101, 101, 67, 100, 49, 84, 49, 71, 107, 50, 109, 112, - 102, 103, 75, 75, 114, 83, 52, 0, 100, 0, 1, 44, 0, 0, 0, 74, 50, 50, 105, 114, 50, - 110, 76, 120, 86, 82, 113, 85, 114, 99, 112, 119, 77, 68, 66, 77, 52, 55, 72, 112, 70, - 67, 76, 76, 82, 114, 75, 70, 114, 111, 70, 54, 76, 106, 75, 55, 68, 69, 65, 1, 0, 0, 0, - 9, 0, 0, 0, 77, 121, 32, 82, 97, 102, 102, 108, 101, 44, 0, 0, 0, 68, 98, 115, 76, 87, - 116, 119, 97, 110, 99, 53, 86, 114, 71, 72, 72, 57, 70, 66, 113, 120, 84, 102, 104, 81, - 65, 49, 88, 120, 67, 105, 116, 115, 81, 67, 65, 99, 97, 104, 97, 100, 101, 57, 114, - 252, 197, 71, 100, 0, 0, 0, 0, 44, 0, 0, 0, 57, 54, 86, 74, 97, 80, 110, 85, 72, 117, - 67, 71, 115, 118, 88, 49, 101, 65, 67, 81, 80, 68, 85, 53, 82, 55, 87, 120, 119, 66, - 50, 84, 120, 81, 89, 54, 67, 76, 57, 52, 55, 68, 113, 70, - ]; - let der = RegistrationAccount::try_from_slice(&bytes).unwrap(); - // let ser = reg_account.try_to_vec().unwrap(); - assert_eq!(der, reg_account); - } -} diff --git a/core/src/types/broadcast_frame.rs b/core/src/types/broadcast_frame.rs new file mode 100644 index 00000000..a9880988 --- /dev/null +++ b/core/src/types/broadcast_frame.rs @@ -0,0 +1,104 @@ +use crate::checkpoint::CheckpointOffChain; +use crate::types::PlayerJoin; +use borsh::{BorshDeserialize, BorshSerialize}; +use race_api::event::{Event, Message}; +use race_api::types::ServerJoin; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +use super::TxState; + +#[derive(Default, Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct BroadcastSync { + pub new_players: Vec, + pub new_servers: Vec, + pub transactor_addr: String, + pub access_version: u64, +} + +impl BroadcastSync { + pub fn new(access_version: u64) -> Self { + Self { + access_version, + ..Default::default() + } + } + + pub fn merge(&mut self, other: &Self) { + self.new_players.append(&mut other.new_players.clone()); + self.new_servers.append(&mut other.new_servers.clone()); + self.access_version = u64::max(self.access_version, other.access_version); + self.transactor_addr = other.transactor_addr.clone(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct EventHistory { + pub event: Event, + pub timestamp: u64, + pub state_sha: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub enum BroadcastFrame { + // Game event + Event { + game_addr: String, + event: Event, + timestamp: u64, + state_sha: String, + }, + // Arbitrary message + Message { + game_addr: String, + message: Message, + }, + // Transaction state updates + TxState { + tx_state: TxState, + }, + // Node state updates + Sync { + sync: BroadcastSync, + }, + // This frame is the first frame in broadcast stream. + EventHistories { + game_addr: String, + checkpoint_off_chain: Option, + histories: Vec, + state_sha: String, + }, +} + +impl Display for BroadcastFrame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BroadcastFrame::Event { event, .. } => { + write!(f, "BroadcastFrame::Event: {}", event) + } + BroadcastFrame::Message { message, .. } => { + write!(f, "BroadcastFrame::Message: {}", message.sender) + } + BroadcastFrame::TxState { tx_state } => { + write!(f, "BroadcastFrame::TxState: {:?}", tx_state) + } + BroadcastFrame::Sync { sync } => { + write!( + f, + "BroadcastFrame::Sync: access_version {}", + sync.access_version + ) + } + BroadcastFrame::EventHistories { histories, .. } => { + write!(f, "BroadcastFrame::EventHistories, len: {}", histories.len()) + } + } + } +} diff --git a/core/src/types/common.rs b/core/src/types/common.rs index 87cc4b10..da1286fc 100644 --- a/core/src/types/common.rs +++ b/core/src/types/common.rs @@ -1,17 +1,30 @@ use borsh::{BorshDeserialize, BorshSerialize}; +use race_api::prelude::InitAccount; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; pub use race_api::types::*; +use crate::context::Node; + pub type SettleTransferCheckpoint = (Vec, Vec, Vec); -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub enum ClientMode { Player, Transactor, Validator, } +#[derive(Debug, PartialEq, Eq, Clone, Copy, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub enum GameMode { + Main, + Sub, +} + #[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Signature { @@ -29,3 +42,49 @@ impl std::fmt::Display for Signature { ) } } + +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] +pub struct SubGameSpec { + pub game_addr: String, + pub game_id: usize, + pub bundle_addr: String, + pub nodes: Vec, + pub access_version: u64, + pub settle_version: u64, + pub init_account: InitAccount, +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct SettleWithAddr { + pub addr: String, + pub op: SettleOp, +} + +impl SettleWithAddr { + pub fn add>(addr: S, amount: u64) -> Self { + Self { + addr: addr.into(), + op: SettleOp::Add(amount), + } + } + pub fn sub>(addr: S, amount: u64) -> Self { + Self { + addr: addr.into(), + op: SettleOp::Sub(amount), + } + } + pub fn eject>(addr: S) -> Self { + Self { + addr: addr.into(), + op: SettleOp::Eject, + } + } + pub fn assign>(addr: S, identifier: String) -> Self { + Self { + addr: addr.into(), + op: SettleOp::AssignSlot(identifier), + } + } +} diff --git a/core/src/types/storage_params.rs b/core/src/types/storage_params.rs new file mode 100644 index 00000000..beb6a5b3 --- /dev/null +++ b/core/src/types/storage_params.rs @@ -0,0 +1,29 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use crate::checkpoint::CheckpointOffChain; + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct SaveCheckpointParams { + pub game_addr: String, + pub settle_version: u64, + pub checkpoint: CheckpointOffChain, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct GetCheckpointParams { + pub game_addr: String, + pub settle_version: u64, +} + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct SaveResult { + pub proof: String, +} diff --git a/core/src/types/transactor_params.rs b/core/src/types/transactor_params.rs index 91b9e821..9565229c 100644 --- a/core/src/types/transactor_params.rs +++ b/core/src/types/transactor_params.rs @@ -1,30 +1,23 @@ //! Parameters for interacting with transactor use crate::encryptor::NodePublicKeyRaw; -use crate::types::PlayerJoin; use borsh::{BorshDeserialize, BorshSerialize}; -use race_api::event::{Event, Message}; +use race_api::event::Event; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use std::fmt::Display; #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub enum TxState { - PlayerConfirming { - confirm_players: Vec, - access_version: u64, - }, - - PlayerConfirmingFailed(u64), +pub struct AttachGameParams { + pub signer: String, + pub key: NodePublicKeyRaw, } #[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct AttachGameParams { - pub signer: String, - pub key: NodePublicKeyRaw, +pub struct CheckpointParams { + pub settle_version: u64, } impl Display for AttachGameParams { @@ -84,36 +77,3 @@ impl Display for SubscribeEventParams { write!(f, "SubscribeEventParams") } } - -#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum BroadcastFrame { - Event { - game_addr: String, - event: Event, - timestamp: u64, - }, - Message { - game_addr: String, - message: Message, - }, - TxState { - tx_state: TxState, - }, -} - -impl Display for BroadcastFrame { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - BroadcastFrame::Event { event, .. } => { - write!(f, "BroadcastFrame::Event: {}", event) - } - BroadcastFrame::Message { message, .. } => { - write!(f, "BroadcastFrame::Message: {}", message.sender) - } - BroadcastFrame::TxState { tx_state } => { - write!(f, "BroadcastFrame::TxState: {:?}", tx_state) - } - } - } -} diff --git a/core/src/types/transport_params.rs b/core/src/types/transport_params.rs index b84db784..7ddbfbdc 100644 --- a/core/src/types/transport_params.rs +++ b/core/src/types/transport_params.rs @@ -4,8 +4,10 @@ use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; -use race_api::types::{RecipientSlotOwner, RecipientSlotType, Settle}; -use super::{common::{EntryType, RecipientSlot, VoteType}, Transfer}; +use race_api::types::{RecipientSlotOwner, RecipientSlotType}; +use crate::checkpoint::CheckpointOnChain; + +use super::{common::{EntryType, RecipientSlot, VoteType}, Transfer, SettleWithAddr}; #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -172,9 +174,9 @@ pub enum AssetChange { #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct SettleParams { pub addr: String, - pub settles: Vec, + pub settles: Vec, pub transfers: Vec, - pub checkpoint: Vec, + pub checkpoint: CheckpointOnChain, pub settle_version: u64, pub next_settle_version: u64, } diff --git a/core/src/types/tx_state.rs b/core/src/types/tx_state.rs new file mode 100644 index 00000000..f2e79cf4 --- /dev/null +++ b/core/src/types/tx_state.rs @@ -0,0 +1,42 @@ +use crate::types::PlayerJoin; +use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct ConfirmingPlayer { + pub id: u64, + pub addr: String, + pub position: u16, + pub balance: u64, +} + +impl From for ConfirmingPlayer { + fn from(value: PlayerJoin) -> Self { + Self { + id: value.access_version, + addr: value.addr, + position: value.position, + balance: value.balance, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub enum TxState { + PlayerConfirming { + confirm_players: Vec, + access_version: u64, + }, + + PlayerConfirmingFailed(u64), + + SettleSucceed { + settle_version: u64, + signature: Option, + }, +} diff --git a/dev/.gitignore b/dev/.gitignore new file mode 100644 index 00000000..90d90cc7 --- /dev/null +++ b/dev/.gitignore @@ -0,0 +1 @@ +test.*.db diff --git a/dev/Justfile b/dev/Justfile new file mode 100644 index 00000000..43fcaf41 --- /dev/null +++ b/dev/Justfile @@ -0,0 +1,14 @@ +holdem-simple: + zellij --layout layouts/holdem-simple.kdl + +holdem-multi: + zellij --layout layouts/holdem-multi.kdl + +durak-simple: + zellij --layout layouts/durak-simple.kdl + +demo-app: + zellij --layout layouts/demo-app.kdl + +mtt-simple: + zellij --layout layouts/mtt-simple.kdl diff --git a/dev/README.md b/dev/README.md index 3f5350fe..ab730aa0 100644 --- a/dev/README.md +++ b/dev/README.md @@ -1,109 +1,3 @@ -# Prepare Solana development environment +# Dev Scripts for Race Development -## Load Metaplex into local test validator - -```bash -solana program dump -u m metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s token_metadata_program.so - -# This will reset the test-ledger -solana-test-validator --bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s token_metadata_program.so --reset -``` - -## Create a token for testing - -```bash -spl-token create-token - -spl-token create-account - -spl-token mint -``` -We use this token for following tests. - -## Deploy RACE contract to solana localnet - -``` shell -just solana -``` - - -## Publish game bundle - -Uploaded NFT metadata on Arweave: - -- Chat: https://arweave.net/6SOGM007lgaeBH1SOJ-Ifm9eUUNz4kCdRqVJe-yl9_g -- Raffle: https://arweave.net/hTlRRw2tZ2q_RpklixjRIzrR3PgAChBujAKzp6wknHs - -```shell -just publish Chat https://arweave.net/6SOGM007lgaeBH1SOJ-Ifm9eUUNz4kCdRqVJe-yl9_g -just publish Raffle https://arweave.net/hTlRRw2tZ2q_RpklixjRIzrR3PgAChBujAKzp6wknHs -``` - -NFT addresses will be returned when upload succeed. Later we will use this NFT token address as **bundle address**. - -## Create game registration - -```shell -just create-reg -``` - -## Create game accounts - -To create games, a spec file in JSON is required. Replace the addresses with what you have, and save it as `spec.json`. - -```json -{ - "title": "", - "reg_addr": "", - "bundle_addr": "", - "token_addr": "", - "max_players": 10, - "min_deposit": 10, - "max_deposit": 20, - "data": [] -} -``` - -Create the game account with command line tool. - -```shell -just create-game spec.json -``` - -Game account address will be returned. - -## Generate a keypair for transactor - -```shell -solana-keygen new -o transactor.json - -# Transfer some SOL to this new account -solana transfer --allow-unfunded-recipient 2 -``` - -## Create transactor configuration file - -```toml -[transactor] -port = 12003 -endpoint = "ws://localhost:12003" -chain = "solana" -address = "" -reg_addresses = [""] - -[solana] -keyfile = /path/to/transactor.json -rpc = "http://localhost:8899" -``` - -## Register server - -``` shell -just dev-reg-transactor /path/to/transactor.toml -``` - -## Start transactor - -```shell -just dev-transactor /path/to/transactor.toml -``` +This folder contains the scripts and configurations for local development on Race Protocol. diff --git a/dev/create-mtt-game.sh b/dev/create-mtt-game.sh deleted file mode 100644 index ba54322b..00000000 --- a/dev/create-mtt-game.sh +++ /dev/null @@ -1,32 +0,0 @@ -TIME=$(date +%s%3N) -START_TIME=$(expr $TIME + 60000) -echo "Current timestamp is $START_TIME" - -DATA=$(cd ./js/borsh; npx ts-node ./bin/cli.ts \ - -u64 "$START_TIME" \ - -u8 2 \ - -u64 10 \ - -u64 60000 \ - -u32 0) -echo "DATA is $DATA" - -JSON=$(cat < /tmp/race-mtt-facade.json -just dev-facade /tmp/race-mtt-facade.json diff --git a/dev/game-specs/holdem-cash.json b/dev/game-specs/holdem-cash.json new file mode 100644 index 00000000..146d2397 --- /dev/null +++ b/dev/game-specs/holdem-cash.json @@ -0,0 +1,13 @@ +{ + "title": "Cash Table", + "bundle": "../race-holdem/target/race_holdem_cash.wasm", + "token": "FACADE_USDC", + "maxPlayers": 6, + "entryType": { + "cash": { + "minDeposit": 100000000, + "maxDeposit": 300000000 + } + }, + "data": [64,66,15,0,0,0,0,0,192,198,45,0,0,0,0,0,0,0,0,0,0,0,0,0,30,0,10,0] +} diff --git a/dev/holdem-multi.kdl b/dev/holdem-multi.kdl deleted file mode 100644 index 14dfb8c3..00000000 --- a/dev/holdem-multi.kdl +++ /dev/null @@ -1,28 +0,0 @@ -// The layout to launch facade and 2 transactors in Zellij - -layout { - pane { - name "Facade" - focus true - cwd "./" - command "just" - args "dev-facade" "../race-holdem/cash/facade.json" - start_suspended true - } - pane split_direction="vertical" { - pane { - name "Transactor 1" - command "just" - cwd "./" - args "facade-transactor" "1" - start_suspended true - } - pane { - name "Transactor 2" - command "just" - cwd "./" - args "facade-transactor" "2" - start_suspended true - } - } -} diff --git a/dev/demo-app.kdl b/dev/layouts/demo-app.kdl similarity index 65% rename from dev/demo-app.kdl rename to dev/layouts/demo-app.kdl index 958ee67c..eff2d2e1 100644 --- a/dev/demo-app.kdl +++ b/dev/layouts/demo-app.kdl @@ -2,19 +2,19 @@ layout { pane split_direction="horizontal" { - pane split_direction="vertical" { + pane split_direction="vertical" { pane { name "Facade" focus true - cwd "./" + cwd "../" command "just" - args "dev-facade" "examples/draw-card/facade.json" + args "dev-facade" "-g" "examples/draw-card/facade.json" "-g" "examples/raffle" start_suspended true } pane { name "Vite" focus true - cwd "./" + cwd "../" command "just" args "dev-demo-app" } @@ -24,15 +24,15 @@ layout { pane { name "Transactor" command "just" - cwd "./" - args "facade-transactor" "1" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" start_suspended true } pane { name "Transactor" command "just" - cwd "./" - args "facade-transactor" "2" + cwd "../" + args "dev-transactor" "dev/server-confs/server2.toml" start_suspended true } } diff --git a/dev/durak-simple.kdl b/dev/layouts/durak-simple.kdl similarity index 66% rename from dev/durak-simple.kdl rename to dev/layouts/durak-simple.kdl index 07ffa970..2b6a43c9 100644 --- a/dev/durak-simple.kdl +++ b/dev/layouts/durak-simple.kdl @@ -5,17 +5,17 @@ layout { pane { name "Facade" focus true - cwd "./" + cwd "../" command "just" - args "dev-facade" "../durak/conf/heads_up.json" "../durak/conf/3players.json" + args "dev-facade" "-g" "../durak/conf/heads_up.json" "-g" "../durak/conf/3players.json" start_suspended true } pane split_direction="horizontal" { pane { name "Transactor" command "just" - cwd "./" - args "facade-transactor" "1" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" start_suspended true } pane diff --git a/dev/layouts/holdem-multi.kdl b/dev/layouts/holdem-multi.kdl new file mode 100644 index 00000000..095405fd --- /dev/null +++ b/dev/layouts/holdem-multi.kdl @@ -0,0 +1,28 @@ +// The layout to launch facade and 2 transactors in Zellij + +layout { + pane { + name "Facade" + focus true + cwd "../" + command "bash" + args "dev/scripts/facade-cash-game.sh" + start_suspended true + } + pane split_direction="vertical" { + pane { + name "Transactor 1" + command "just" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" + start_suspended true + } + pane { + name "Transactor 2" + command "just" + cwd "../" + args "dev-transactor" "dev/server-confs/server2.toml" + start_suspended true + } + } +} diff --git a/dev/holdem-simple.kdl b/dev/layouts/holdem-simple.kdl similarity index 54% rename from dev/holdem-simple.kdl rename to dev/layouts/holdem-simple.kdl index 7dc584ff..d1810e13 100644 --- a/dev/holdem-simple.kdl +++ b/dev/layouts/holdem-simple.kdl @@ -2,20 +2,20 @@ layout { pane split_direction="vertical" { - pane { - name "Facade" - focus true - cwd "./" - command "just" - args "dev-facade" "../race-holdem/cash/facade.json" - start_suspended true - } + pane { + name "Facade" + focus true + cwd "../" + command "bash" + args "dev/scripts/facade-cash-game.sh" + start_suspended true + } pane split_direction="horizontal" { pane { name "Transactor" command "just" - cwd "./" - args "facade-transactor" "1" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" start_suspended true } pane diff --git a/dev/mtt-simple.kdl b/dev/layouts/mtt-simple.kdl similarity index 72% rename from dev/mtt-simple.kdl rename to dev/layouts/mtt-simple.kdl index 9efb057d..6eac0bd4 100644 --- a/dev/mtt-simple.kdl +++ b/dev/layouts/mtt-simple.kdl @@ -5,17 +5,17 @@ layout { pane { name "Facade" focus true - cwd "./" + cwd "../" command "bash" - args "dev/create-mtt-game.sh" + args "dev/scripts/facade-mtt-game.sh" start_suspended true } pane split_direction="horizontal" { pane { name "Transactor" command "just" - cwd "./" - args "facade-transactor" "1" + cwd "../" + args "dev-transactor" "dev/server-confs/server1.toml" start_suspended true } pane diff --git a/dev/prepare.sh b/dev/prepare.sh deleted file mode 100755 index 15ea079d..00000000 --- a/dev/prepare.sh +++ /dev/null @@ -1,133 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) - -set -e - -echo "Preparing the local environment..." -echo "The keypair at $HOME/.config/solana/id.json will be used" -echo "Create a SPL token for testing..." - - -if solana config get | grep 'RPC URL: http://localhost:8899'; -then - echo "Confirmed LOCAL network is used" -else - echo "Exit due to mainnet is used" - exit 1 -fi - -echo "Airdrop 10SOL..." -solana airdrop 10 - -TOKEN=$(spl-token create-token --decimals 0 | grep 'Address' | awk '{print $2}') -echo "Token: $TOKEN will be used for games" -spl-token create-account "$TOKEN" -spl-token mint $TOKEN 1000000 - -mkdir -p dist -echo "Generate game bundle NFT metadata..." -cat < dist/race_example_raffle_metadata.json -{ - "name": "RACE Example Raffle", - "symbol": "RACE", - "description": "Example raffle game", - "seller_fee_basis_points": 0, - "image": "https://arweave.net/OQx85OOqT0oawKjCS9f5PowBF_5Y8hkJ8DI5fvMjd-s", - "external_url": "", - "attributes": [], - "properties": { - "files": [ - { - "uri": "https://arweave.net/OQx85OOqT0oawKjCS9f5PowBF_5Y8hkJ8DI5fvMjd-s", - "type": "image/png" - }, - { - "uri": "http://127.0.0.1:8000/race_example_raffle.wasm", - "type": "application/wasm" - } - ], - "category": "image", - "creators": [ - { - "address": "J22ir2nLxVRqUrcpwMDBM47HpFCLLRrKFroF6LjK7DEA", - "share": 100 - } - ] - } -} -EOF - -echo "Compile Raffle example..." -just example-raffle -cp ../target/race_example_raffle.wasm dist/ - -echo "Publishing WASM as game bundle NFT" -BUNDLE=$(just publish Raffle 'http://127.0.0.1:8000/race_example_raffle_metadata.json' | grep 'Address' | awk '{print $2}') -echo "Raffle is published as NFT at $BUNDLE" - -echo "Create registration center..." -REG=$(just create-reg | grep 'Address' | awk '{print $2}') -echo "Registration: $REG will be used" - -echo "Generate game spec..." -cat < dist/raffle.spec.json -{ - "title": "My Raffle", - "reg_addr": "$REG", - "bundle_addr": "$BUNDLE", - "token_addr": "$TOKEN", - "max_players": 10, - "min_deposit": 1, - "max_deposit": 2, - "data": [] -} -EOF - -echo "Create game..." -GAME=$(just create-game $SCRIPT_DIR/dist/raffle.spec.json | grep 'Address' | awk '{print $2}') -echo "A game of raffle is created at $GAME" - -echo "Generate a keypair for transactor..." -solana-keygen new -o dist/transactor-keypair.json --no-bip39-passphrase -s -f -# solana airdrop 10 dist/transactor-keypair.json -TRANSACTOR=$(solana-keygen pubkey dist/transactor-keypair.json) -solana airdrop 10 dist/transactor-keypair.json --commitment finalized - -echo "Generate server configuration..." -TX_PATH=$SCRIPT_DIR/dist/transactor-keypair.json -cat < dist/transactor.toml -[transactor] -port = 12003 -endpoint = "ws://localhost:12003" -chain = "solana" -address = "$TRANSACTOR" -reg_addresses = ["$REG"] - -[solana] -keyfile = "$TX_PATH" -rpc = "http://localhost:8899" -EOF - -echo "Create server account..." -just dev-reg-transactor $SCRIPT_DIR/dist/transactor.toml - -echo "Generate demo-app-data..." -cat < dist/demo-app-data.json -{ - "CHAIN_TO_REG_ADDR": { - "solana-local": "$REG" - }, - "CHAIN_ADDR_GAME_MAPPING": { - "solana-local": { - "$BUNDLE": "raffle" - } - } -} -EOF - -echo "That's it! -Start a server to simulate AR storage and provide data for demo-app: simple-http-server --cors -- dist -Now you can start the transactor with: just dev-transactor ${SCRIPT_DIR}/dist/transactor.toml -And open the demo app with: just ts-sdk dev-sdk dev-demo-app -" diff --git a/dev/scripts/facade-cash-game.sh b/dev/scripts/facade-cash-game.sh new file mode 100644 index 00000000..a5daee96 --- /dev/null +++ b/dev/scripts/facade-cash-game.sh @@ -0,0 +1,4 @@ +echo "Delete test db files" +rm dev/test.*.db +echo "Start facade server" +just dev-facade -g dev/game-specs/holdem-cash.json diff --git a/dev/scripts/facade-mtt-game.sh b/dev/scripts/facade-mtt-game.sh new file mode 100644 index 00000000..83249571 --- /dev/null +++ b/dev/scripts/facade-mtt-game.sh @@ -0,0 +1,51 @@ +TIME=$(date +%s%3N) +START_TIME=$(expr $TIME + 30000) +TICKET=100000000 +TABLE_SIZE=3 +START_CHIPS=10000 + +echo "Delete test db files" +rm dev/test.*.db + +echo "Current timestamp is $START_TIME" +echo "Ticket is $TICKET" +echo "Table size is $TABLE_SIZE" +echo "Start chips is $START_CHIPS" + + +DATA=$(cd ./js/borsh; npx ts-node ./bin/cli.ts \ + -u64 "$START_TIME" \ + -u64 "$TICKET" \ + -u8 "$TABLE_SIZE" \ + -u64 "$START_CHIPS" \ + -u64 50 \ + -u64 60000 \ + -u32 0 \ + -u32 3 \ + -u8 50 \ + -u8 30 \ + -u8 20 \ + -u8 0) +echo "DATA is $DATA" + +JSON=$(cat < /tmp/race-mtt-facade.json +echo "Start facade server" +just dev-facade -g /tmp/race-mtt-facade.json -b ../race-holdem/target/race_holdem_mtt_table.wasm diff --git a/examples/conf/server1.toml b/dev/server-confs/server1.toml similarity index 82% rename from examples/conf/server1.toml rename to dev/server-confs/server1.toml index 8ca83997..0262d1a7 100644 --- a/examples/conf/server1.toml +++ b/dev/server-confs/server1.toml @@ -5,8 +5,12 @@ chain = "facade" address = "Server 1" reg_addresses = ["DEFAULT_REGISTRATION_ADDRESS"] disable_blacklist = true +debug_mode = true [facade] host = "http://localhost:12002" registration_address = "DEFAULT_REGISTRATION_ADDRESS" address = "Server 1" + +[storage] +db_file_name = "dev/test.1.db" diff --git a/examples/conf/server2.toml b/dev/server-confs/server2.toml similarity index 87% rename from examples/conf/server2.toml rename to dev/server-confs/server2.toml index c40260f7..d0200dc4 100644 --- a/examples/conf/server2.toml +++ b/dev/server-confs/server2.toml @@ -10,3 +10,6 @@ disable_blacklist = true host = "http://localhost:12002" registration_address = "DEFAULT_REGISTRATION_ADDRESS" address = "Server 2" + +[storage] +db_file_name = "dev/test.2.db" diff --git a/encryptor/Cargo.toml b/encryptor/Cargo.toml index 813ebeb8..cb9dc100 100644 --- a/encryptor/Cargo.toml +++ b/encryptor/Cargo.toml @@ -19,7 +19,7 @@ publish = true race-core = { workspace = true } race-api = { workspace = true } thiserror = { workspace = true } -sha1 = { workspace = true } +sha2 = { workspace = true } rand = { workspace = true } base64 = { workspace = true } arrayref = { workspace = true } diff --git a/encryptor/src/lib.rs b/encryptor/src/lib.rs index 9c291383..ff9f7b77 100644 --- a/encryptor/src/lib.rs +++ b/encryptor/src/lib.rs @@ -25,7 +25,7 @@ use race_core::types::{Ciphertext, SecretDigest, SecretKey}; use race_core::encryptor::{EncryptorError, EncryptorResult, EncryptorT, NodePublicKeyRaw}; use race_core::types::Signature; use rand::seq::SliceRandom; -use sha1::{Digest, Sha1}; +use sha2::{Digest, Sha256}; // Since we use different secrets for each encryption, // we can use a fixed IV. @@ -360,7 +360,7 @@ impl EncryptorT for Encryptor { } fn digest(&self, text: &[u8]) -> SecretDigest { - Sha1::digest(text).to_vec() + Sha256::digest(text).to_vec() } fn export_public_key(&self, addr: Option<&str>) -> EncryptorResult { diff --git a/env/src/config.rs b/env/src/config.rs index 15cdb660..b4a21d35 100644 --- a/env/src/config.rs +++ b/env/src/config.rs @@ -32,11 +32,18 @@ pub struct TransactorConfig { pub address: String, pub reg_addresses: Vec, pub disable_blacklist: Option, + pub debug_mode: Option, +} + +#[derive(Deserialize)] +pub struct StorageConfig { + pub db_file_name: String, } #[derive(Deserialize)] pub struct Config { pub transactor: Option, + pub storage: Option, pub facade: Option, pub solana: Option, pub bnb: Option, diff --git a/examples/chat/Cargo.toml b/examples/chat/Cargo.toml deleted file mode 100644 index 6aff5199..00000000 --- a/examples/chat/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "race-example-chat" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib"] - -[dependencies] -race-core = { path = "../../core" } -race-proc-macro = { path = "../../proc-macro" } -serde_json = "1.0.85" -serde = "1.0.144" -borsh = "0.9.3" diff --git a/examples/chat/chat_bundle_metadata.json b/examples/chat/chat_bundle_metadata.json deleted file mode 100644 index 30dc031f..00000000 --- a/examples/chat/chat_bundle_metadata.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "RACE Exampel Chat", - "symbol": "RACE", - "description": "Exameple chat game", - "seller_fee_basis_points": 0, - "image": "https://arweave.net/OQx85OOqT0oawKjCS9f5PowBF_5Y8hkJ8DI5fvMjd-s", - "external_url": "", - "attributes": [], - "properties": { - "files": [ - { - "uri": "https://arweave.net/OQx85OOqT0oawKjCS9f5PowBF_5Y8hkJ8DI5fvMjd-s", - "type": "image/png" - }, - { - "uri": "https://arweave.net/sd50KiTrdGzWdX9TQ_QHpGWmWe5ZgfSHTfnSYTXpmho", - "type": "application/wasm" - } - ], - "category": "image", - "creators": [ - { - "address": "JDirZbxCJxb4GVeKaGbeZcsr7UgfLZQCCc11hFabT9pW", - "share": 100 - } - ] - } -} diff --git a/examples/chat/src/lib.rs b/examples/chat/src/lib.rs deleted file mode 100644 index a780c34f..00000000 --- a/examples/chat/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -use race_core::prelude::*; - -#[derive(Serialize, Deserialize)] -enum GameEvent { - Message { text: String }, -} - -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -struct Message { - sender: String, - text: String, -} - -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize)] -#[game_handler] -struct Chat { - messages: Vec, -} - -impl GameHandler for Chat { - /// Initialize handler state with on-chain game account data. - fn init_state(context: &mut Effect, _init_account: InitAccount) -> Result { - Ok(Self { messages: vec![] }) - } - - /// Handle event. - fn handle_event(&mut self, _context: &mut Effect, event: Event) -> Result<()> { - match event { - Event::Custom { sender, raw } => { - let event: GameEvent = serde_json::from_str(&raw).or(Err(Error::JsonParseError))?; - match event { - GameEvent::Message { text } => { - self.messages.push(Message { sender, text }); - } - } - } - _ => (), - } - Ok(()) - } -} diff --git a/examples/draw-card/src/integration_test.rs b/examples/draw-card/src/integration_test.rs index dc6d2854..1763300f 100644 --- a/examples/draw-card/src/integration_test.rs +++ b/examples/draw-card/src/integration_test.rs @@ -4,98 +4,66 @@ use race_test::prelude::*; #[test] fn test() -> anyhow::Result<()> { - // Initialize player client, which simulates the behavior of player. - println!("Create test clients"); + // Initialize player clients, which simulates the behavior of players. let mut alice = TestClient::player("Alice"); let mut bob = TestClient::player("Bob"); // Initialize the client, which simulates the behavior of transactor. - let mut transactor = TestClient::transactor("transactor"); + let transactor_addr = "Transactor".to_string(); + let mut transactor = TestClient::transactor(&transactor_addr); - // Initialize the game account, with 1 player joined. - // The game account must be served, so we add one server which is the transactor. - let account_data = AccountData { + // Initialize the test context, which 1 player joined. + // The game must be served by at least one server, so we add a transactor here. + let (mut ctx, _) = TestContextBuilder::default() + .with_data(AccountData { blind_bet: 100, min_bet: 100, max_bet: 1000, - }; - println!("Create game account"); - let game_account = TestGameAccountBuilder::default() - .set_transactor(&transactor) - .add_player(&alice, 10000) - .with_data(account_data) - .build(); - let transactor_addr = game_account.transactor_addr.as_ref().unwrap().clone(); + }) + .with_max_players(2) + .set_transactor(&mut transactor) + .add_player(&mut alice, 10000) + .build_with_init_state::()?; - // Create game context and test handler. - // Initalize the handler state with game account. // The game will not start since two players are required. - println!("Initialize handler state"); - let mut ctx = GameContext::try_new(&game_account)?; - let mut handler = TestHandler::init_state(&mut ctx, &game_account)?; - assert_eq!(1, ctx.count_players()); - - // Start game + // Start game should fail println!("Start game, without players"); - let first_event = ctx.gen_start_game_event(); - let ret = handler.handle_event(&mut ctx, &first_event); + let ret = ctx.handle_event(&Event::GameStart); assert_eq!(ret, Err(Error::HandleError(HandleError::NoEnoughPlayers))); { - let state: &DrawCard = handler.get_state(); + let state: &DrawCard = ctx.state(); assert_eq!(0, state.bet); } // Another player joined the game. // Now we have enough players, an event of `GameStart` should be dispatched. println!("Player Bob join the game"); - let av = ctx.get_access_version() + 1; - let sync_event = Event::Sync { - new_players: vec![PlayerJoin { - addr: "Bob".into(), - balance: 10000, - position: 1, - access_version: av, - verify_key: "".into(), - }], - new_servers: vec![], - transactor_addr: transactor.get_addr(), - access_version: av, - }; - - handler.handle_event(&mut ctx, &sync_event)?; + let join_event = ctx.join(&mut bob, 10000); + let ee = ctx.handle_event(&join_event)?; { - assert_eq!(2, ctx.count_players()); - assert_eq!(GameStatus::Uninit, ctx.get_status()); - assert_eq!( - Some(DispatchEvent::new( - Event::GameStart { - access_version: ctx.get_access_version() - }, - 0 - )), - *ctx.get_dispatch() - ); + println!("State: {:?}", ctx.state()); + assert!(ee.start_game); } // Now the dispatching event should be `GameStart`. // By handling this event, a random deck of cards should be created. println!("Start game"); - handler.handle_dispatch_event(&mut ctx)?; + ctx.handle_dispatch_event()?; { - let state: &DrawCard = handler.get_state(); - assert_eq!(GameStatus::Running, ctx.get_status()); + let state: &DrawCard = ctx.state(); + println!("State: {:?}", state); assert_eq!(1, state.random_id); assert_eq!( vec![ Player { - addr: "Bob".into(), + id: bob.id(), balance: 10000, bet: 0 }, Player { - addr: "Alice".into(), + id: alice.id(), balance: 10000, bet: 0 }, @@ -104,7 +72,7 @@ fn test() -> anyhow::Result<()> { ); assert_eq!( RandomStatus::Masking(transactor_addr.clone()), - ctx.get_random_state_unchecked(1).status + ctx.random_state(1)?.status ); } @@ -114,52 +82,52 @@ fn test() -> anyhow::Result<()> { // Now, Let the client handle the updated context. // The corresponding secert state will be initialized, which contains all the secrets. // Additionally, one `Mask` event will be created. - let events = transactor.handle_updated_context(&ctx)?; + let events = ctx.client_events(&mut transactor)?; { assert_eq!(1, transactor.secret_state().list_random_secrets().len()); assert_eq!(1, events.len()); } // Send the mask event to handler, we expect the random status to be changed to `Locking`. - handler.handle_event(&mut ctx, &events[0])?; + ctx.handle_event(&events[0])?; { assert_eq!( RandomStatus::Locking(transactor_addr.clone()), - ctx.get_random_state_unchecked(1).status + ctx.random_state(1)?.status ); } // Now, Let the client handle the updated context. // One `Lock` event will be created. - let events = transactor.handle_updated_context(&ctx)?; + let events = ctx.client_events(&mut transactor)?; { assert_eq!(1, events.len()); } // Send the lock event to handler, we expect the random status to be changed to `Ready`. // Since all randomness is ready, an event of `RandomnessReady` will be dispatched. - handler.handle_event(&mut ctx, &events[0])?; + ctx.handle_event(&events[0])?; { assert_eq!( RandomStatus::Ready, - ctx.get_random_state_unchecked(1).status + ctx.random_state(1)?.status ); assert_eq!( Some(DispatchEvent::new( Event::RandomnessReady { random_id: 1 }, 0 )), - *ctx.get_dispatch() + ctx.current_dispatch() ); } // Handle this dispatched `RandomnessReady`. // We expect each player to be assigned one card. - handler.handle_dispatch_event(&mut ctx)?; + ctx.handle_dispatch_event()?; { - let random_state = ctx.get_random_state_unchecked(1); + let random_state = ctx.random_state(1)?; let ciphertexts_for_alice = random_state.list_assigned_ciphertexts("Alice"); let ciphertexts_for_bob = random_state.list_assigned_ciphertexts("Bob"); assert_eq!(RandomStatus::WaitingSecrets, random_state.status); @@ -170,42 +138,42 @@ fn test() -> anyhow::Result<()> { // Let client handle the updated context. // `ShareSecret` event should be created. println!("Transactor handle updated context, generate `ShareSecrets` event"); - let events = transactor.handle_updated_context(&ctx)?; + let events = ctx.client_events(&mut transactor)?; { let event = &events[0]; assert!( - matches!(event, Event::ShareSecrets { sender, shares } if sender.eq(&transactor_addr) && shares.len() == 2) + matches!(event, Event::ShareSecrets { sender, shares } if *sender == transactor.id() && shares.len() == 2) ); } // Handle `ShareSecret` event. // Expect the random status to be changed to ready. - handler.handle_event(&mut ctx, &events[0])?; + ctx.handle_event(&events[0])?; { assert_eq!( RandomStatus::Ready, - ctx.get_random_state_unchecked(1).status + ctx.random_state(1)?.status ); - let random_state = ctx.get_random_state_unchecked(1); + let random_state = ctx.random_state(1)?; assert_eq!(1, random_state.list_shared_secrets("Alice").unwrap().len()); assert_eq!(1, random_state.list_shared_secrets("Bob").unwrap().len()); assert_eq!( Some(DispatchEvent::new(Event::SecretsReady { random_ids: vec![1] }, 0)), - *ctx.get_dispatch() + ctx.current_dispatch() ); } println!("Dispatch `SecretsReady` event, update game stage to `Betting`"); - handler.handle_dispatch_event(&mut ctx)?; + ctx.handle_dispatch_event()?; { - let state = handler.get_state(); + let state = ctx.state(); assert_eq!(GameStage::Betting, state.stage); } // Now, client should be able to see their cards. println!("Checked assigned cards"); - let alice_decryption = alice.decrypt(&ctx, 1)?; - let bob_decryption = bob.decrypt(&ctx, 1)?; + let alice_decryption = ctx.client_decrypt(&alice, 1)?; + let bob_decryption = ctx.client_decrypt(&bob, 1)?; { println!("Alice decryption: {:?}", alice_decryption); println!("Bob decryption: {:?}", bob_decryption); @@ -213,23 +181,23 @@ fn test() -> anyhow::Result<()> { assert_eq!(1, bob_decryption.len()); } - // Now, Alice is the first to act. + // Now, Bob is the first to act. // So, she can send a bet event and we expect the bet amount to be updated to 500. println!("Bob bets"); let event = bob.custom_event(GameEvent::Bet(500)); - handler.handle_event(&mut ctx, &event)?; + ctx.handle_event(&event)?; { - let state = handler.get_state(); + let state = ctx.state(); assert_eq!(500, state.bet); } - // Bob calls this. + // Alice calls this. // Now, it's time to reveal the cards, so two secrets for hands are required. println!("Alice calls"); let event = alice.custom_event(GameEvent::Call); - handler.handle_event(&mut ctx, &event)?; + ctx.handle_event(&event)?; { - let random_state = ctx.get_random_state_unchecked(1); + let random_state = ctx.random_state(1)?; assert_eq!( 2, random_state @@ -240,59 +208,55 @@ fn test() -> anyhow::Result<()> { // Let the client handle this update. // We expect two secrets to be shared. - let events = transactor.handle_updated_context(&ctx)?; + let events = ctx.client_events(&mut transactor)?; { let event = &events[0]; println!( "Required ident: {:?}", - ctx.get_random_state_unchecked(1) + ctx.random_state(1)? .list_required_secrets_by_from_addr(&transactor_addr) ); assert_eq!( 2, - ctx.get_random_state_unchecked(1) + ctx.random_state(1)? .list_required_secrets_by_from_addr(&transactor_addr) .len() ); assert!( - matches!(event, Event::ShareSecrets { sender, shares } if sender.eq(&transactor_addr) && shares.len() == 2) + matches!(event, Event::ShareSecrets { sender, shares } if *sender == transactor.id() && shares.len() == 2) ); } // Handle `ShareSecret` event. println!("Dispatch ShareSecret event."); - handler.handle_event(&mut ctx, &events[0])?; + + ctx.handle_event(&events[0])?; { println!( "Required ident: {:?}", - ctx.get_random_state_unchecked(1) + ctx.random_state(1)? .list_required_secrets_by_from_addr(&transactor_addr) ); assert_eq!( 0, - ctx.get_random_state_unchecked(1) + ctx.random_state(1)? .list_required_secrets_by_from_addr(&transactor_addr) .len() ); assert_eq!( Some(DispatchEvent::new(Event::SecretsReady { random_ids: vec![1] }, 0)), - *ctx.get_dispatch() + ctx.current_dispatch() ); } // Now, the transactor should be able to reveal all hole cards. - let decryption = transactor.decrypt(&ctx, 1)?; + let decryption = ctx.client_decrypt(&transactor, 1)?; println!("Decryption: {:?}", decryption); - println!("Secrets Ready Event: {:?}", ctx.get_dispatch()); + println!("Secrets Ready Event: {:?}", ctx.current_dispatch()); assert_eq!(2, decryption.len()); // Now send `SecretReady` to handler. - handler.handle_dispatch_event(&mut ctx)?; - { - assert!(matches!(ctx.get_settles(), - Some(settles) if settles.len() == 2 - )); - } + ctx.handle_dispatch_event()?; Ok(()) } diff --git a/examples/draw-card/src/lib.rs b/examples/draw-card/src/lib.rs index cd5e11d6..994ef188 100644 --- a/examples/draw-card/src/lib.rs +++ b/examples/draw-card/src/lib.rs @@ -13,7 +13,7 @@ use race_api::prelude::*; use race_proc_macro::game_handler; const ACTION_TIMEOUT: u64 = 30_000; -const NEXT_GAME_TIMEOUT: u64 = 15_000; +const NEXT_GAME_TIMEOUT: u64 = 3000; #[derive(BorshSerialize, BorshDeserialize)] pub enum GameEvent { @@ -44,7 +44,7 @@ pub enum GameStage { #[cfg_attr(test, derive(Debug, PartialEq, Eq))] #[derive(BorshSerialize, BorshDeserialize)] pub struct Player { - pub addr: String, + pub id: u64, pub balance: u64, pub bet: u64, } @@ -64,9 +64,6 @@ pub struct DrawCard { pub max_bet: u64, } -#[derive(BorshSerialize, BorshDeserialize)] -pub struct DrawCardCheckpoint {} - impl DrawCard { fn set_winner(&mut self, effect: &mut Effect, winner_index: usize) -> Result<(), HandleError> { let players = array_mut_ref![self.players, 0, 2]; @@ -75,12 +72,12 @@ impl DrawCard { let player_1 = &mut player_1[0]; if winner_index == 0 { - effect.settle(Settle::add(&player_0.addr, self.pot - player_0.bet)); - effect.settle(Settle::sub(&player_1.addr, player_1.bet)); + effect.settle(Settle::add(player_0.id, self.pot - player_0.bet))?; + effect.settle(Settle::sub(player_1.id, player_1.bet))?; player_0.balance += self.pot; } else { - effect.settle(Settle::add(&player_1.addr, self.pot - player_1.bet)); - effect.settle(Settle::sub(&player_0.addr, player_0.bet)); + effect.settle(Settle::add(player_1.id, self.pot - player_1.bet))?; + effect.settle(Settle::sub(player_0.id, player_0.bet))?; player_1.balance += self.pot; } @@ -92,7 +89,7 @@ impl DrawCard { fn custom_handle_event( &mut self, effect: &mut Effect, - sender: String, + sender: u64, event: GameEvent, ) -> Result<(), HandleError> { match event { @@ -102,7 +99,7 @@ impl DrawCard { .players .get_mut(0) .ok_or(HandleError::Custom("Player not found".into()))?; - if sender.ne(&player.addr) { + if sender != player.id { return Err(HandleError::InvalidPlayer); } if amount < self.min_bet || amount > self.max_bet || amount > player.balance { @@ -113,7 +110,7 @@ impl DrawCard { self.bet = amount; self.pot += amount; self.stage = GameStage::Reacting; - effect.action_timeout(player.addr.clone(), ACTION_TIMEOUT); + effect.action_timeout(player.id.clone(), ACTION_TIMEOUT)?; } else { return Err(HandleError::Custom("Can't bet".into())); } @@ -124,7 +121,7 @@ impl DrawCard { .players .get_mut(1) .ok_or(HandleError::Custom("Player not found".into()))?; - if sender.ne(&player.addr) { + if sender.ne(&player.id) { return Err(HandleError::InvalidPlayer); } if self.bet > player.balance { @@ -172,8 +169,6 @@ fn is_better_than(card_a: &str, card_b: &str) -> bool { impl GameHandler for DrawCard { - type Checkpoint = DrawCardCheckpoint; - fn init_state(_effect: &mut Effect, init_account: InitAccount) -> Result { let AccountData { blind_bet, @@ -184,7 +179,7 @@ impl GameHandler for DrawCard { .players .into_iter() .map(|p| Player { - addr: p.addr, + id: p.id, balance: p.balance, bet: 0, }) @@ -214,18 +209,18 @@ impl GameHandler for DrawCard { // Waiting timeout usually sent after each game. Here we // can trigger the next game. Event::WaitingTimeout => { - if effect.count_players() == 2 { + if self.players.len() == 2 { effect.start_game(); } } // Reset current game state. Set up randomness Event::GameStart { .. } => { - if effect.count_players() < 2 { + if self.players.len() < 2 { return Err(HandleError::NoEnoughPlayers); } - if effect.servers_count == 0 { + if effect.count_nodes() == 0 { return Err(HandleError::NoEnoughServers); } @@ -243,15 +238,15 @@ impl GameHandler for DrawCard { } Event::RandomnessReady { .. } => { - effect.assign(self.random_id, &self.players[0].addr, vec![0]); - effect.assign(self.random_id, &self.players[1].addr, vec![1]); + effect.assign(self.random_id, self.players[0].id, vec![0])?; + effect.assign(self.random_id, self.players[1].id, vec![1])?; } // Start game when there are two players. - Event::Sync { new_players, .. } => { - for p in new_players.into_iter() { + Event::Join { players } => { + for p in players.into_iter() { self.players.push(Player { - addr: p.addr, + id: p.id, balance: p.balance, bet: 0, }); @@ -268,7 +263,7 @@ impl GameHandler for DrawCard { // Now it's the first player's turn to act. // So we dispatch an action timeout event. self.stage = GameStage::Betting; - effect.action_timeout(self.players[0].addr.clone(), ACTION_TIMEOUT); + effect.action_timeout(self.players[0].id.clone(), ACTION_TIMEOUT)?; effect.allow_exit(true); } GameStage::Revealing => { @@ -291,11 +286,11 @@ impl GameHandler for DrawCard { } } - Event::Leave { player_addr } => { - if let Some(player_idx) = self.players.iter().position(|p| p.addr.eq(&player_addr)) + Event::Leave { player_id } => { + if let Some(player_idx) = self.players.iter().position(|p| p.id.eq(&player_id)) { self.set_winner(effect, if player_idx == 0 { 1 } else { 0 })?; - effect.settle(Settle::eject(player_addr)); + effect.settle(Settle::eject(player_id))?; effect.checkpoint(); } else { return Err(HandleError::InvalidPlayer); @@ -306,10 +301,6 @@ impl GameHandler for DrawCard { Ok(()) } - - fn into_checkpoint(self) -> HandleResult { - Ok(DrawCardCheckpoint {}) - } } #[cfg(test)] @@ -325,7 +316,7 @@ mod tests { max_bet: 1000, }; - let data = account_data.try_to_vec().unwrap(); + let data = borsh::to_vec(&account_data).unwrap(); println!("data: {:?}", data); println!("data len: {}", data.len()); } diff --git a/examples/minimal/minimal.wasm b/examples/minimal/minimal.wasm index dfd8a700..0ff34735 100644 Binary files a/examples/minimal/minimal.wasm and b/examples/minimal/minimal.wasm differ diff --git a/examples/minimal/src/lib.rs b/examples/minimal/src/lib.rs index 639c6e8d..6175555b 100644 --- a/examples/minimal/src/lib.rs +++ b/examples/minimal/src/lib.rs @@ -19,11 +19,6 @@ struct Minimal { n: u64, } -#[derive(BorshDeserialize, BorshSerialize)] -pub struct MinimalCheckpoint { - n: u64 -} - impl Minimal { fn handle_custom_event(&mut self, event: MinimalEvent) -> Result<(), HandleError> { match event { @@ -34,9 +29,11 @@ impl Minimal { } impl GameHandler for Minimal { - type Checkpoint = MinimalCheckpoint; fn init_state(_effect: &mut Effect, init_account: InitAccount) -> Result { + if let Some(checkpoint) = init_account.checkpoint()? { + return Ok(checkpoint); + } let account_data: MinimalAccountData = init_account.data()?; Ok(Self { n: account_data.init_n, @@ -52,8 +49,4 @@ impl GameHandler for Minimal { _ => Ok(()), } } - - fn into_checkpoint(self) -> Result { - Ok(MinimalCheckpoint { n: self.n }) - } } diff --git a/examples/raffle/facade.json b/examples/raffle/facade.json index 169c0ff2..383a884a 100644 --- a/examples/raffle/facade.json +++ b/examples/raffle/facade.json @@ -1,9 +1,13 @@ { - "title": "1RACE Raffle", + "title": "Simple Raffle", "bundle": "target/race_example_raffle.wasm", "token": "FACADE_USDT", "max_players": 6, - "min_deposit": 100, - "max_deposit": 100, + "entryType": { + "cash": { + "min_deposit": 100, + "max_deposit": 100 + } + }, "data": [] } diff --git a/examples/raffle/src/lib.rs b/examples/raffle/src/lib.rs index bc2a5d44..3dc85de2 100644 --- a/examples/raffle/src/lib.rs +++ b/examples/raffle/src/lib.rs @@ -12,14 +12,14 @@ const DRAW_TIMEOUT: u64 = 30_000; #[cfg_attr(test, derive(Debug, PartialEq, Eq))] #[derive(BorshSerialize, BorshDeserialize)] struct Player { - pub addr: String, + pub id: u64, pub balance: u64, } -impl From for Player { - fn from(value: PlayerJoin) -> Self { +impl From for Player { + fn from(value: GamePlayer) -> Self { Self { - addr: value.addr, + id: value.id, balance: value.balance, } } @@ -28,7 +28,7 @@ impl From for Player { #[derive(BorshSerialize, BorshDeserialize)] #[game_handler] struct Raffle { - last_winner: Option, + last_winner: Option, players: Vec, random_id: RandomId, draw_time: u64, @@ -44,8 +44,6 @@ impl Raffle { impl GameHandler for Raffle { - type Checkpoint = (); - /// Initialize handler state with on-chain game account data. fn init_state(_effect: &mut Effect, init_account: InitAccount) -> HandleResult { let players = init_account.players.into_iter().map(Into::into).collect(); @@ -63,15 +61,15 @@ impl GameHandler for Raffle { match event { Event::GameStart { .. } => { // We need at least one player to start, otherwise we will skip this draw. - if effect.count_players() >= 1 { - let options = self.players.iter().map(|p| p.addr.to_owned()).collect(); + if self.players.len() >= 1 { + let options = self.players.iter().map(|p| p.id.to_string()).collect(); let rnd_spec = RandomSpec::shuffled_list(options); self.random_id = effect.init_random_state(rnd_spec); } } - Event::Sync { new_players, .. } => { - let players = new_players.into_iter().map(Into::into); + Event::Join { players } => { + let players = players.into_iter().map(Into::into); self.players.extend(players); if self.players.len() >= 1 && self.draw_time == 0 { self.draw_time = effect.timestamp() + DRAW_TIMEOUT; @@ -79,7 +77,7 @@ impl GameHandler for Raffle { } } - // Reveal the first address when randomness is ready. + // Reveal the first idess when randomness is ready. Event::RandomnessReady { .. } => { effect.reveal(self.random_id, vec![0]); } @@ -101,14 +99,15 @@ impl GameHandler for Raffle { .get_revealed(self.random_id)? .get(&0) .unwrap() - .to_owned(); + .parse::() + .unwrap(); for p in self.players.iter() { - if p.addr.ne(&winner) { - effect.settle(Settle::add(&winner, p.balance)); - effect.settle(Settle::sub(&p.addr, p.balance)); + if p.id != winner { + effect.settle(Settle::add(winner, p.balance))?; + effect.settle(Settle::sub(p.id, p.balance))?; } - effect.settle(Settle::eject(&p.addr)); + effect.settle(Settle::eject(p.id))?; } self.last_winner = Some(winner); self.cleanup(); @@ -117,10 +116,6 @@ impl GameHandler for Raffle { } Ok(()) } - - fn into_checkpoint(self) -> HandleResult<()> { - Ok(()) - } } #[cfg(test)] @@ -146,26 +141,12 @@ mod tests { players: vec![], random_id: 0, }; - let event = Event::Sync { - new_players: vec![PlayerJoin { - addr: "alice".into(), - position: 0, - balance: 100, - access_version: 0, - verify_key: "".into(), - }], - new_servers: vec![ServerJoin { - addr: "foo".into(), - endpoint: "foo.endpoint".into(), - access_version: 0, - verify_key: "".into(), - }], - transactor_addr: "".into(), - access_version: 0, + let event = Event::Join { + players: vec![GamePlayer::new(0, 100, 0), GamePlayer::new(1, 100, 1)], }; state.handle_event(&mut effect, event).unwrap(); - assert_eq!(state.players.len(), 1); + assert_eq!(state.players.len(), 2); assert_eq!(effect.wait_timeout, Some(DRAW_TIMEOUT)); } } diff --git a/examples/simple-settle/src/lib.rs b/examples/simple-settle/src/lib.rs index 9bfd4b7f..2d258de6 100644 --- a/examples/simple-settle/src/lib.rs +++ b/examples/simple-settle/src/lib.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; #[game_handler] #[derive(BorshSerialize, BorshDeserialize)] struct SimpleSettle { - players: BTreeMap, + players: BTreeMap, } #[derive(BorshSerialize, BorshDeserialize)] @@ -24,11 +24,11 @@ impl SimpleSettle { if self.players.len() > 1 { let (ref winner, _) = self.players.pop_first().unwrap(); let win: u64 = self.players.values().sum(); - effect.settle(Settle::add(winner, win)); - effect.settle(Settle::eject(winner)); + effect.settle(Settle::add(*winner, win)); + effect.settle(Settle::eject(*winner)); for p in self.players.iter() { - effect.settle(Settle::sub(p.0, *p.1)); - effect.settle(Settle::eject(p.0)); + effect.settle(Settle::sub(*p.0, *p.1)); + effect.settle(Settle::eject(*p.0)); } self.players.clear(); } else { @@ -45,7 +45,7 @@ impl GameHandler for SimpleSettle { let players = init_account .players .into_iter() - .map(|p| (p.addr, p.balance)) + .map(|p| (p.id, p.balance)) .collect(); let mut ins = Self { players }; ins.maybe_settle(effect)?; @@ -54,9 +54,9 @@ impl GameHandler for SimpleSettle { fn handle_event(&mut self, effect: &mut Effect, event: Event) -> Result<(), HandleError> { match event { - Event::Sync { new_players, .. } => { - for p in new_players { - self.players.insert(p.addr, p.balance); + Event::Join { players, .. } => { + for p in players { + self.players.insert(p.id, p.balance); } self.maybe_settle(effect)?; Ok(()) @@ -65,7 +65,7 @@ impl GameHandler for SimpleSettle { } } - fn into_checkpoint(self) -> Result { + fn checkpoint(self) -> Result { Ok(Checkpoint {}) } } @@ -74,22 +74,23 @@ impl GameHandler for SimpleSettle { #[cfg(test)] mod tests { use super::*; - use race_test::prelude::sync_new_players; #[test] pub fn test_settle() { let mut effect = Effect::default(); - let event = sync_new_players(&[ - ("alice", 0, 100), - ("bob", 1, 100) - ], 1); + let event = Event::Join { + players: vec![ + GamePlayer::new(0, 100, 0), + GamePlayer::new(1, 100, 1), + ] + }; let mut handler = SimpleSettle::init_state(&mut effect, InitAccount::default()).unwrap(); handler.handle_event(&mut effect, event).unwrap(); assert_eq!(effect.settles, vec![ - Settle::add("alice", 100), - Settle::eject("alice"), - Settle::sub("bob", 100), - Settle::eject("bob"), + Settle::add(0, 100), + Settle::eject(0), + Settle::sub(1, 100), + Settle::eject(1), ]); } } diff --git a/facade/src/main.rs b/facade/src/main.rs index 44dac093..14ca44c5 100644 --- a/facade/src/main.rs +++ b/facade/src/main.rs @@ -2,7 +2,7 @@ //! It is supposed to be used for testing and developing. use borsh::BorshSerialize; -use clap::Parser; +use clap::{arg, Command}; use hyper::Method; use jsonrpsee::server::{AllowHosts, ServerBuilder, ServerHandle}; use jsonrpsee::types::Params; @@ -10,8 +10,8 @@ use jsonrpsee::{core::Error as RpcError, RpcModule}; use race_api::error::Error; use race_core::types::{ DepositParams, EntryType, GameAccount, GameBundle, GameRegistration, PlayerDeposit, PlayerJoin, - PlayerProfile, RecipientSlot, RegistrationAccount, ServerAccount, ServerJoin, SettleOp, - SettleParams, TokenAccount, Vote, VoteParams, VoteType, + PlayerProfile, RecipientAccount, RecipientSlot, RegistrationAccount, ServerAccount, ServerJoin, + SettleOp, SettleParams, TokenAccount, Vote, VoteParams, VoteType, }; use regex::Regex; use serde::Deserialize; @@ -20,7 +20,7 @@ use std::fs::File; use std::io::Read; use std::net::SocketAddr; use std::sync::Arc; -use std::time::{Duration, UNIX_EPOCH}; +use std::time::UNIX_EPOCH; use tokio::sync::Mutex; use tower::ServiceBuilder; use tower_http::cors::{Any, CorsLayer}; @@ -116,6 +116,7 @@ pub struct Context { servers: HashMap, games: HashMap, bundles: HashMap, + recipients: HashMap, } #[derive(Clone, BorshSerialize)] @@ -135,9 +136,15 @@ pub struct PlayerInfo { } impl Context { - pub fn load_games(&mut self, spec_paths: Vec) { - for spec_path in spec_paths.into_iter() { - self.add_game(&spec_path); + pub fn load_games(&mut self, spec_paths: &[&str]) { + for spec_path in spec_paths.iter() { + self.add_game(spec_path); + } + } + + pub fn load_bundles(&mut self, bundle_paths: &[&str]) { + for bundle_path in bundle_paths.iter() { + self.add_bundle(bundle_path) } } @@ -177,6 +184,21 @@ impl Context { }); } + fn add_bundle(&mut self, bundle_path: &str) { + let re = Regex::new(r"[^a-zA-Z0-9]").unwrap(); + let bundle_addr = re.replace_all(&bundle_path, "").into_owned(); + let mut f = File::open(bundle_path).expect(&format!("Bundle {} not found", &bundle_path)); + let mut data = vec![]; + f.read_to_end(&mut data).unwrap(); + let bundle = GameBundle { + name: bundle_addr.clone(), + uri: "".into(), + data, + }; + self.bundles.insert(bundle_addr.clone(), bundle); + println!("+ Bundle: {}", bundle_addr); + } + fn add_game(&mut self, spec_path: &str) { let f = File::open(spec_path).expect("Spec file not found"); let GameSpec { @@ -191,6 +213,7 @@ impl Context { let re = Regex::new(r"[^a-zA-Z0-9]").unwrap(); let bundle_addr = re.replace_all(&bundle, "").into_owned(); let game_addr = re.replace_all(&spec_path, "").into_owned(); + let recipient_addr = format!("{}_recipient", game_addr); let mut f = File::open(&bundle).expect(&format!("Bundle {} not found", &bundle)); let mut data = vec![]; f.read_to_end(&mut data).unwrap(); @@ -210,8 +233,13 @@ impl Context { entry_type, ..Default::default() }; + let recipient = RecipientAccount { + addr: recipient_addr.clone(), + ..Default::default() + }; self.bundles.insert(bundle_addr.clone(), bundle); self.games.insert(game_addr.clone(), game); + self.recipients.insert(recipient_addr.clone(), recipient); println!("! Load game from `{}`", spec_path); println!("+ Game: {}", game_addr); println!("+ Bundle: {}", bundle_addr); @@ -229,7 +257,7 @@ async fn get_game_bundle( let addr: String = params.one()?; let context = context.lock().await; if let Some(bundle) = context.bundles.get(&addr) { - Ok(Some(bundle.to_owned().try_to_vec().unwrap())) + Ok(borsh::to_vec(&bundle).ok()) } else { println!("? get_game_bundle, addr: {}, not found", addr); Ok(None) @@ -253,15 +281,14 @@ async fn get_registration_info( }) .collect(); Ok(Some( - RegistrationAccount { + borsh::to_vec(&RegistrationAccount { addr, is_private: false, size: 100, owner: None, games, - } - .try_to_vec() - .unwrap(), + }) + .unwrap(), )) } @@ -299,6 +326,20 @@ async fn join(params: Params<'_>, context: Arc>) -> RpcResult<()> return Err(custom_error(Error::PlayerAlreadyJoined(player_addr))); } else { game_account.access_version += 1; + // Find available position + let mut pos_list = vec![position]; + pos_list.extend(0..100); + let position = pos_list + .into_iter() + .find(|p| { + game_account + .players + .iter() + .find(|player| player.position == *p) + .is_none() + }) + .unwrap(); + let player_join = PlayerJoin { addr: player_addr.clone(), position, @@ -322,6 +363,8 @@ async fn join(params: Params<'_>, context: Arc>) -> RpcResult<()> EntryType::Ticket { slot_id, amount } => todo!(), #[allow(unused)] EntryType::Gating { collection } => todo!(), + #[allow(unused)] + EntryType::Disabled => todo!(), } } else { return Err(custom_error(Error::GameAccountNotFound)); @@ -370,7 +413,7 @@ async fn get_server_info( let addr: String = params.one()?; let context = context.lock().await; if let Some(server) = context.servers.get(&addr) { - Ok(Some(server.to_owned().try_to_vec().unwrap())) + Ok(Some(borsh::to_vec(&server).unwrap())) } else { println!("? get_server_info, addr: {}, not found", addr); Ok(None) @@ -471,7 +514,7 @@ async fn get_profile( let addr: String = params.one()?; let context = context.lock().await; match context.players.get(&addr) { - Some(player_info) => Ok(Some(player_info.profile.clone().try_to_vec().unwrap())), + Some(player_info) => Ok(Some(borsh::to_vec(&player_info.profile).unwrap())), None => Ok(None), } } @@ -596,6 +639,8 @@ async fn serve(params: Params<'_>, context: Arc>) -> RpcResult<() .get_mut(&game_addr) .ok_or(custom_error(Error::GameAccountNotFound))?; + let new_access_version = account.access_version + 1; + if account.transactor_addr.is_none() { is_transactor = true; account.transactor_addr = Some(server_addr.clone()); @@ -621,11 +666,11 @@ async fn serve(params: Params<'_>, context: Arc>) -> RpcResult<() DEFAULT_MAX_SERVERS as _, ))); } else { - account.access_version += 1; + account.access_version = new_access_version; account.servers.push(ServerJoin::new( server_addr.clone(), server_account.endpoint.clone(), - account.access_version, + new_access_version, verify_key, )); } @@ -653,7 +698,7 @@ async fn get_balance(params: Params<'_>, context: Arc>) -> RpcRes } else { println!("? get_balance, player_addr: {}, not found", player_addr); } - Ok(amount.try_to_vec().unwrap()) + Ok(borsh::to_vec(&amount).unwrap()) } async fn get_account_info( @@ -663,7 +708,7 @@ async fn get_account_info( let addr: String = params.one()?; let context = context.lock().await; if let Some(account) = context.games.get(&addr) { - Ok(Some(account.to_owned().try_to_vec().unwrap())) + Ok(Some(borsh::to_vec(&account).unwrap())) } else { println!("? get_account_info, addr: {}, not found", addr); Ok(None) @@ -673,7 +718,7 @@ async fn get_account_info( async fn list_tokens(_params: Params<'_>, context: Arc>) -> RpcResult> { let context = context.lock().await; let tokens: Vec<&TokenAccount> = context.tokens.values().collect(); - let bytes = tokens.try_to_vec()?; + let bytes = borsh::to_vec(&tokens)?; Ok(bytes) } @@ -686,10 +731,22 @@ async fn get_player_info( let Some(player) = context.players.get(&addr) else { return Ok(None); }; - Ok(Some(player.try_to_vec().unwrap())) + Ok(Some(borsh::to_vec(player).unwrap())) +} + +async fn get_recipient( + params: Params<'_>, + context: Arc>, +) -> RpcResult>> { + let addr: String = params.one()?; + let context = context.lock().await; + let Some(recipient) = context.recipients.get(&addr) else { + return Ok(None); + }; + Ok(Some(borsh::to_vec(recipient).unwrap())) } -async fn settle(params: Params<'_>, context: Arc>) -> RpcResult<()> { +async fn settle(params: Params<'_>, context: Arc>) -> RpcResult { let SettleParams { addr, settles, @@ -704,7 +761,7 @@ async fn settle(params: Params<'_>, context: Arc>) -> RpcResult<( ); // Simulate the finality time - tokio::time::sleep(Duration::from_secs(1)).await; + // tokio::time::sleep(Duration::from_secs(10)).await; // --- let mut context = context.lock().await; @@ -727,6 +784,7 @@ async fn settle(params: Params<'_>, context: Arc>) -> RpcResult<( .retain(|d| d.settle_version < game.settle_version); if game.settle_version != settle_version { + println!("E The settle_versions mismach"); return Err(custom_error(Error::InvalidSettle(format!( "Invalid settle version, current: {}, transaction: {}", game.settle_version, settle_version, @@ -736,8 +794,7 @@ async fn settle(params: Params<'_>, context: Arc>) -> RpcResult<( // Increase the `settle_version` game.settle_version = next_settle_version; println!("! Bump settle version to {}", game.settle_version); - game.checkpoint = checkpoint; - game.checkpoint_access_version = game.access_version; + game.checkpoint_on_chain = Some(checkpoint); // Handle settles for s in settles.into_iter() { @@ -793,7 +850,7 @@ async fn settle(params: Params<'_>, context: Arc>) -> RpcResult<( context.players = players; context.games = games; - Ok(()) + Ok(format!("facade_settle_{}", settle_version)) } async fn run_server(context: Context) -> anyhow::Result { @@ -816,6 +873,8 @@ async fn run_server(context: Context) -> anyhow::Result { module.register_async_method("get_game_bundle", get_game_bundle)?; module.register_async_method("get_registration_info", get_registration_info)?; module.register_async_method("get_balance", get_balance)?; + module.register_async_method("get_player_info", get_player_info)?; + module.register_async_method("get_recipient", get_recipient)?; module.register_async_method("register_server", register_server)?; module.register_async_method("create_profile", create_profile)?; module.register_async_method("get_profile", get_profile)?; @@ -826,26 +885,30 @@ async fn run_server(context: Context) -> anyhow::Result { module.register_async_method("settle", settle)?; module.register_async_method("vote", vote)?; module.register_async_method("list_tokens", list_tokens)?; - module.register_async_method("get_player_info", get_player_info)?; let handle = http_server.start(module)?; Ok(handle) } -/// Command-line interface -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - // Game specs to load - specs: Vec, +fn cli() -> Command { + Command::new("facade") + .about("A mock server for local development with Race") + .arg(arg!(-g ... "The path to a game spec json file")) + .arg(arg!(-b ... "The path to a wasm bundle")) } #[tokio::main] async fn main() -> anyhow::Result<()> { - let args = Args::parse(); + println!("Start at {}", HTTP_HOST); + let matches = cli().get_matches(); let mut context = Context::default(); - context.load_games(args.specs); context.load_default_tokens(); + if let Some(game_spec_paths) = matches.get_many::("game") { + context.load_games(&game_spec_paths.map(String::as_str).collect::>()); + } + if let Some(bundle_paths) = matches.get_many::("bundle") { + context.load_bundles(&bundle_paths.map(String::as_str).collect::>()); + } let server_handle = run_server(context).await?; server_handle.stopped().await; Ok(()) diff --git a/flake.lock b/flake.lock index ef35d92b..ce966b91 100644 --- a/flake.lock +++ b/flake.lock @@ -5,29 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1687709756, - "narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -38,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1688585123, - "narHash": "sha256-+xFOB4WaRUHuZI7H1tWHTrwY4BnbPmh8M1n/XhPRH0w=", + "lastModified": 1725194671, + "narHash": "sha256-tLGCFEFTB5TaOKkpfw3iYT9dnk4awTP/q4w+ROpMfuw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "23de9f3b56e72632c628d92b71c47032e14a3d4d", + "rev": "b833ff01a0d694b910daca6e2ff4a3f26dee478c", "type": "github" }, "original": { @@ -54,11 +36,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1681358109, - "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", + "lastModified": 1718428119, + "narHash": "sha256-WdWDpNaq6u1IPtxtYHHWpl5BmabtpmLnMAx0RdJ/vo8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", + "rev": "e6cea36f83499eb4e9cd184c8a8e823296b50ad5", "type": "github" }, "original": { @@ -77,15 +59,14 @@ }, "rust-overlay": { "inputs": { - "flake-utils": "flake-utils_2", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1688610886, - "narHash": "sha256-Ir5FaBvhBtZ5OGZ3g9rgy4twUcEnyuGEz4QLmxAn9FE=", + "lastModified": 1725243956, + "narHash": "sha256-0A5ZP8uDCyBdYUzayZfy6JFdTefP79oZVAjyqA/yuSI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "358d7155300cd64357f9afd14aa3383c2323e5fc", + "rev": "a10c8092d5f82622be79ed4dd12289f72011f850", "type": "github" }, "original": { @@ -108,21 +89,6 @@ "repo": "default", "type": "github" } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ec6d8ce9..acfe5ce8 100644 --- a/flake.nix +++ b/flake.nix @@ -14,42 +14,28 @@ pkgs = import nixpkgs { inherit system overlays; }; - code = pkgs.callPackage ./. { inherit nixpkgs system rust-overlay; }; - in rec { - packages = { - race-transactor = code.race-transactor; - race-cli = code.race-cli; - all = pkgs.symlinkJoin { - name = "all"; - paths = with code; [ race-transactor race-cli ]; - }; - }; - - default = packages.all; - + in { devShell = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - (rust-bin.stable.latest.default.override { + (rust-bin.stable."1.77.0".default.override { extensions = [ "rust-src" ]; - targets = [ "wasm32-unknown-unknown" ]; + targets = [ "wasm32-unknown-unknown" ]; }) cargo openssl pkg-config - rust-analyzer - simple-http-server nodejs_18 just binaryen + # For development + rust-analyzer + nodePackages.typescript nodePackages.typescript-language-server + zellij ]; - RUST_LOG = "info,wasmer_compiler_cranelift=info,solana_rpc_client=debug,solana_client=debug,jsonrpsee_server=info"; + RUST_LOG = "info,hyper=error,wasmer_compiler_cranelift=info,solana_rpc_client=debug,solana_client=debug,jsonrpsee_server=info"; RUST_BACKTRACE = 1; }; } ); - - nixConfig = { - bash-prompt-prefix = "[race]"; - }; } diff --git a/js/borsh/src/errors.ts b/js/borsh/src/errors.ts index 07a8790e..fb86ca71 100644 --- a/js/borsh/src/errors.ts +++ b/js/borsh/src/errors.ts @@ -1,3 +1,7 @@ +export function invalidCtor(path: string[]) { + throw new Error(`Borsh: Cannot deserialize, missing type annotation at: ${path.join(',')}`); +} + export function invalidByteArrayLength(path: string[], expected: number, actual: number) { throw new Error(`Borsh: Invalid byte array length at: ${path.join(',')}, expected: ${expected}, actual: ${actual}`); } diff --git a/js/borsh/src/index.ts b/js/borsh/src/index.ts index 6a14e316..c07058e3 100644 --- a/js/borsh/src/index.ts +++ b/js/borsh/src/index.ts @@ -17,7 +17,37 @@ import { } from './types'; import { BinaryWriter } from './writer'; import { BinaryReader } from './reader'; -import { invalidByteArrayLength, extendedWriterNotFound, extendedReaderNotFound, invalidEnumField } from './errors'; +import { invalidByteArrayLength, extendedWriterNotFound, extendedReaderNotFound, invalidEnumField, invalidCtor } from './errors'; + +class DeserializeError extends Error { + cause: Error; + path: string[]; + obj: any | undefined; + + constructor(path: string[], cause: Error, obj: any) { + super('Deserialize failed') + this.cause = cause; + this.path = path; + this.obj = obj; + Object.setPrototypeOf(this, DeserializeError.prototype) + } +} + +class SerializeError extends Error { + cause: Error; + path: string[]; + fieldType: FieldType; + value: any; + + constructor(path: string[], cause: Error, fieldType: FieldType, value: any) { + super('Serialize failed') + this.cause = cause; + this.path = path; + this.fieldType = fieldType; + this.value = value; + Object.setPrototypeOf(this, SerializeError.prototype) + } +} function addSchemaField(prototype: any, key: FieldKey, fieldType: FieldType) { let fields: Field[] = prototype.__schema_fields || []; @@ -93,7 +123,7 @@ function serializeValue(path: string[], value: any, fieldType: FieldType, writer } else if (kind === 'array') { writer.writeU32(value.length); for (let i = 0; i < value.length; i++) { - serializeValue([...path, ``], value[i], v, writer); + serializeValue([...path, ``], value[i], v, writer); } } else if (kind === 'struct') { serializeStruct(path, value, writer); @@ -115,8 +145,11 @@ function serializeValue(path: string[], value: any, fieldType: FieldType, writer } } } catch (err: any) { - console.error('Borsh serialize failed, path:', path, 'error:', err); - throw err; + if (err instanceof SerializeError) { + throw err; + } else { + throw new SerializeError(path, err, fieldType, value) + } } } @@ -154,7 +187,7 @@ function deserializeValue(path: string[], fieldType: FieldType, reader: BinaryRe let arr = []; const length = reader.readU32(); for (let i = 0; i < length; i++) { - let v = deserializeValue([...path, ``], value, reader); + let v = deserializeValue([...path, ``], value, reader); arr.push(v); } return arr; @@ -189,8 +222,11 @@ function deserializeValue(path: string[], fieldType: FieldType, reader: BinaryRe } } } catch (err: any) { - console.error('Borsh deserialize failed, path:', path, 'error:', err); - throw err; + if (err instanceof DeserializeError) { + throw err + } else { + throw new DeserializeError(path, err, undefined) + } } } @@ -234,24 +270,37 @@ function deserializeEnum(path: string[], enumClass: Function, reader: BinaryRead if (enumVariants instanceof Array) { const i = reader.readU8(); const variant = enumVariants[i]; - return deserializeStruct([...path, ``], variant, reader); + let obj = deserializeStruct([...path, ``], variant, reader); + Object.setPrototypeOf(obj, variant.prototype); + return obj; } else { invalidEnumField(path); } } function deserializeStruct(path: string[], ctor: Ctor, reader: BinaryReader): T { + if (ctor === undefined) invalidCtor(path); const prototype = ctor.prototype; const fields = getSchemaFields(prototype); let obj = {}; - for (const field of fields) { - obj = deserializeField(path, obj, field, reader); + try { + for (const field of fields) { + obj = deserializeField(path, obj, field, reader); + } + } catch (e) { + if (e instanceof DeserializeError) { + if (e.obj === undefined) { + e.obj = obj; + } + } + throw e; } return new ctor(obj); } export function field(fieldType: FieldType) { return function (target: any, key: PropertyKey) { + if (target?.constructor?.prototype === undefined) throw new Error(`Invalid field argument for key: ${key.toString()}`) addSchemaField(target.constructor.prototype, key, fieldType); }; } @@ -288,10 +337,17 @@ export function enums(enumClass: Function): EnumFieldType { export function serialize(obj: any): Uint8Array { const writer = new BinaryWriter(); - if (isVariantObject(obj)) { - serializeEnum([], obj, writer); - } else { - serializeStruct([], obj, writer); + try { + if (isVariantObject(obj)) { + serializeEnum([], obj, writer); + } else { + serializeStruct([], obj, writer); + } + } catch (e) { + if (e instanceof SerializeError) { + console.error('Serialize failed, path:', e.path, ', fieldType:', e.fieldType, ', value:', e.value, ', cause:', e.cause); + } + throw e; } return writer.toArray(); } @@ -300,10 +356,17 @@ export function deserialize(enumClass: EnumClass, data: Uint8Array): T; export function deserialize(ctor: Ctor, data: Uint8Array): T; export function deserialize(classType: Ctor | EnumClass, data: Uint8Array): T { const reader = new BinaryReader(data); - if (isEnumClass(classType)) { - return deserializeEnum([], classType, reader); - } else { - return deserializeStruct([], classType, reader); + try { + if (isEnumClass(classType)) { + return deserializeEnum([], classType, reader); + } else { + return deserializeStruct([], classType, reader); + } + } catch (e) { + if (e instanceof DeserializeError) { + console.error('Deserialize failed, path:', e.path, ', current object:', e.obj, ', cause:', e.cause, ', data:', data); + } + throw e; } } diff --git a/js/borsh/tsfmt.json b/js/borsh/tsfmt.json index ac46672c..5823aaa0 120000 --- a/js/borsh/tsfmt.json +++ b/js/borsh/tsfmt.json @@ -1 +1 @@ -/home/tianshu/workspace/race/js/config/tsfmt.json \ No newline at end of file +../config/tsfmt.json \ No newline at end of file diff --git a/js/package-lock.json b/js/package-lock.json index 39bc1606..81d01035 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -44,6 +44,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -58,17 +67,89 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.21.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", @@ -115,12 +196,12 @@ "dev": true }, "node_modules/@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.21.4", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -149,34 +230,34 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -235,30 +316,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -288,13 +369,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -373,9 +454,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -574,34 +655,34 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -609,13 +690,13 @@ } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -666,23 +747,23 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -704,9 +785,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -743,21 +824,47 @@ } }, "node_modules/@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "peer": true, + "bin": { + "rlp": "bin/rlp" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "peer": true, + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -779,9 +886,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1141,6 +1248,18 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", "dev": true }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "peer": true, + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", @@ -1154,16 +1273,16 @@ "peer": true }, "node_modules/@noble/hashes": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", - "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "peer": true + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@noble/secp256k1": { "version": "1.7.1", @@ -1228,6 +1347,42 @@ "resolved": "sdk-solana", "link": true }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "peer": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "peer": true, + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "peer": true, + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -1321,12 +1476,16 @@ } }, "node_modules/@swc/core": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.60.tgz", - "integrity": "sha512-dWfic7sVjnrStzGcMWakHd2XPau8UXGPmFUTkx6xGX+DOVtfAQVzG6ZW7ohw/yNcTqI05w6Ser26XMTMGBgXdA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", + "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", "dev": true, "hasInstallScript": true, "peer": true, + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, "engines": { "node": ">=10" }, @@ -1335,16 +1494,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.60", - "@swc/core-darwin-x64": "1.3.60", - "@swc/core-linux-arm-gnueabihf": "1.3.60", - "@swc/core-linux-arm64-gnu": "1.3.60", - "@swc/core-linux-arm64-musl": "1.3.60", - "@swc/core-linux-x64-gnu": "1.3.60", - "@swc/core-linux-x64-musl": "1.3.60", - "@swc/core-win32-arm64-msvc": "1.3.60", - "@swc/core-win32-ia32-msvc": "1.3.60", - "@swc/core-win32-x64-msvc": "1.3.60" + "@swc/core-darwin-arm64": "1.3.101", + "@swc/core-darwin-x64": "1.3.101", + "@swc/core-linux-arm-gnueabihf": "1.3.101", + "@swc/core-linux-arm64-gnu": "1.3.101", + "@swc/core-linux-arm64-musl": "1.3.101", + "@swc/core-linux-x64-gnu": "1.3.101", + "@swc/core-linux-x64-musl": "1.3.101", + "@swc/core-win32-arm64-msvc": "1.3.101", + "@swc/core-win32-ia32-msvc": "1.3.101", + "@swc/core-win32-x64-msvc": "1.3.101" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -1356,9 +1515,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.60.tgz", - "integrity": "sha512-oCDKWGdSO1WyErduGfiITRDoq7ZBt9PXETlhi8BGKH/wCc/3mfSNI9wXAg3Stn8mrT0lUJtdsnwMI/eZp6dK+A==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", + "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", "cpu": [ "arm64" ], @@ -1373,9 +1532,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.60.tgz", - "integrity": "sha512-pcE/1oUlmN/BkKndOPtViqTkaM5pomagXATo+Muqn4QNMnkSOEVcmF9T3Lr3nB1A7O/fwCew3/aHwZ5B2TZ1tA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", + "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", "cpu": [ "x64" ], @@ -1390,9 +1549,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.60.tgz", - "integrity": "sha512-Moc+86SWcbPr06PaQYUb0Iwli425F7QgjwTCNEPYA6OYUsjaJhXMaHViW2WdGIXue2+eaQbg31BHQd14jXcoBg==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", + "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", "cpu": [ "arm" ], @@ -1407,9 +1566,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.60.tgz", - "integrity": "sha512-pPGZrTgSXBvp6IrXPXz8UJr82AElf8hMuK4rNHmLGDCqrWnRIFLUpiAsc2WCFIgdwqitZNQoM+F2vbceA/bkKg==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", + "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", "cpu": [ "arm64" ], @@ -1424,9 +1583,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.60.tgz", - "integrity": "sha512-HSFQaVUkjWYNsQeymAQ3IPX3csRQvHe6MFyqPfvCCQ4dFlxPvlS7VvNaLnGG+ZW1ek7Lc+hEX+4NGzZKsxDIHA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", + "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", "cpu": [ "arm64" ], @@ -1441,9 +1600,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.60.tgz", - "integrity": "sha512-WJt/X6HHM3/TszckRA7UKMXec3FHYsB9xswQbIYxN4bfTQodu3Rc8bmpHYtFO7ScMLrhY+RljHLK6wclPvaEXw==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", + "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", "cpu": [ "x64" ], @@ -1458,9 +1617,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.60.tgz", - "integrity": "sha512-DGGBqAPUXy/aPMBKokL3osZC9kM97HchiDPuprzwgTMP40YQ3hGCzNJ5jK7sOk9Tc4PEdZ2Igfr9sBHmCrxxQw==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", + "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", "cpu": [ "x64" ], @@ -1475,9 +1634,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.60.tgz", - "integrity": "sha512-wQg/BZPJvp5WpUbsBp7VHjhUh0DfYOPhP6dH67WO9QQ07+DvOk2DR2Bfh0z0ts1k7H/FsAqExWtTDCWMCRJiRQ==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", + "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", "cpu": [ "arm64" ], @@ -1492,9 +1651,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.60.tgz", - "integrity": "sha512-nqkd0XIVyGbnBwAxP4GIfx6n45/hAPETpmQYpDSGnucOKFJfvGdFGL81GDG1acPCq/oFtR3tIyTbPpKmJ0N6xQ==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", + "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", "cpu": [ "ia32" ], @@ -1509,9 +1668,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.60.tgz", - "integrity": "sha512-ouw+s22i9PYQpSE7Xc+ZittEyA87jElXABesviSpP+jgHt10sM5KFUpVAeV8DRlxJCXMJJ5AhOdCf4TAtFr+6A==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", + "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", "cpu": [ "x64" ], @@ -1525,6 +1684,13 @@ "node": ">=10" } }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true, + "peer": true + }, "node_modules/@swc/register": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@swc/register/-/register-0.1.10.tgz", @@ -1542,6 +1708,13 @@ "@swc/core": "^1.0.46" } }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true, + "peer": true + }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -1674,6 +1847,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, "node_modules/@types/node": { "version": "12.20.55", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", @@ -1709,6 +1888,21 @@ "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", "dev": true }, + "node_modules/@types/rfdc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/rfdc/-/rfdc-1.2.0.tgz", + "integrity": "sha512-9Q81yCQwj4iuqDWVEcZMaY3QJ+w+Vj3M71ZdobgFC6T+RowLlE5CaLwRrwMiFvE3LZ7Qy1SDrR+EjGtVu9FmJQ==", + "deprecated": "This is a stub types definition. rfdc provides its own type definitions, so you do not need this installed.", + "dependencies": { + "rfdc": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1739,89 +1933,383 @@ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz", + "integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/type-utils": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, "engines": { - "node": ">=0.4.0" + "node": ">=10" } }, - "node_modules/agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "peer": true, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 8.0.0" + "node": ">=10" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz", + "integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz", + "integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==", "dev": true, "dependencies": { - "type-fest": "^0.21.3" + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0" }, "engines": { - "node": ">=8" + "node": "^16.0.0 || >=18.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@typescript-eslint/type-utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz", + "integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==", "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, "engines": { - "node": ">=8" - } + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz", + "integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz", + "integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz", + "integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz", + "integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.15.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, "node_modules/ansi-styles": { "version": "4.3.0", @@ -1879,6 +2367,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -2190,6 +2687,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-reverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz", + "integrity": "sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==", + "peer": true + }, "node_modules/bufferutil": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", @@ -2426,6 +2929,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "peer": true + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2539,6 +3048,27 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2697,27 +3227,28 @@ } }, "node_modules/eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2725,22 +3256,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "bin": { @@ -2754,9 +3282,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -2770,9 +3298,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -2842,19 +3370,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/eslint/node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2870,23 +3385,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -2902,27 +3400,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/eslint/node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint/node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -2936,12 +3413,12 @@ } }, "node_modules/espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "dependencies": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" }, @@ -3007,6 +3484,59 @@ "node": ">=0.10.0" } }, + "node_modules/ethereum-bloom-filters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.2.0.tgz", + "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", + "peer": true, + "dependencies": { + "@noble/hashes": "^1.4.0" + } + }, + "node_modules/ethereum-bloom-filters/node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "peer": true, + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/ethjs-unit/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "peer": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -3076,6 +3606,34 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3371,6 +3929,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3389,10 +3967,10 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has": { @@ -3744,6 +4322,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "peer": true, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, "node_modules/is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -4589,16 +5177,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4715,6 +5293,19 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4857,6 +5448,37 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/merkletreejs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.4.0.tgz", + "integrity": "sha512-a48Ta5kWiVNBgeEbZVMm6FB1hBlp6vEuou/XnZdlkmd2zq6NZR6Sh2j+kR1B0iOZIXrTMcigBYzZ39MLdYhm1g==", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.1", + "buffer-reverse": "^1.0.1", + "crypto-js": "^4.2.0", + "treeify": "^1.1.0", + "web3-utils": "^1.3.4" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "peer": true + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -5178,6 +5800,26 @@ "node": ">=8" } }, + "node_modules/number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", + "peer": true, + "dependencies": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, + "node_modules/number-to-bn/node_modules/bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "peer": true + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -5238,6 +5880,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5433,6 +6092,15 @@ "node": ">=8" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/prettier": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", @@ -5488,9 +6156,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -5532,6 +6200,15 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -5641,6 +6318,11 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6016,6 +6698,19 @@ "node": ">=6" } }, + "node_modules/strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "peer": true, + "dependencies": { + "is-hex-prefixed": "1.0.0" + }, + "engines": { + "node": ">=6.5.0", + "npm": ">=3" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6123,6 +6818,27 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "peer": true }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-chacha20": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-chacha20/-/ts-chacha20-1.2.0.tgz", @@ -6257,6 +6973,18 @@ "node": ">=0.3.1" } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -6293,16 +7021,16 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=14.17" } }, "node_modules/unbox-primitive": { @@ -6379,6 +7107,12 @@ "node": ">=6.14.2" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "peer": true + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -6430,7 +7164,26 @@ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "dependencies": { - "makeerror": "1.0.12" + "makeerror": "1.0.12" + } + }, + "node_modules/web3-utils": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "peer": true, + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereum-cryptography": "^2.1.2", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + }, + "engines": { + "node": ">=8.0.0" } }, "node_modules/webidl-conversions": { @@ -6500,15 +7253,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -6633,19 +7377,26 @@ "name": "@race-foundation/sdk-core", "version": "0.2.6", "license": "ISC", + "dependencies": { + "@types/rfdc": "^1.2.0", + "rfdc": "^1.4.1" + }, "devDependencies": { "@types/chai": "^4.3.4", "@types/jest": "^29.5.1", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", "chai": "^4.3.7", - "eslint": "^8.40.0", + "eslint": "^8.56.0", "jest": "^29.5.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", "ts-jest": "^29.1.0", - "typescript": "^5.0.4" + "typescript": "^5.3.3" }, "peerDependencies": { "@race-foundation/borsh": "*", + "merkletreejs": "^0.4.0", "ts-chacha20": "^1.2.0" } }, @@ -6695,6 +7446,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -6706,12 +7463,71 @@ } }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -6752,12 +7568,12 @@ } }, "@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.21.4", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -6777,28 +7593,28 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -6842,24 +7658,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -6880,13 +7696,13 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -6949,9 +7765,9 @@ } }, "@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -7090,42 +7906,42 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.6.tgz", + "integrity": "sha512-czastdK1e8YByZqezMPFiZ8ahwVMh/ESl9vPgvgdB9AmFMGP5jfpFax74AQgl5zj4XHzqeYAg2l8PuUeRS1MgQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -7166,20 +7982,20 @@ } }, "@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true }, "@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.5.2", + "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -7195,9 +8011,9 @@ "dev": true }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -7221,18 +8037,35 @@ } }, "@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, + "@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "peer": true + }, + "@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "peer": true, + "requires": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + } + }, "@humanwhocodes/config-array": { - "version": "0.11.8", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", - "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -7244,9 +8077,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -7535,6 +8368,15 @@ } } }, + "@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "peer": true, + "requires": { + "@noble/hashes": "1.4.0" + } + }, "@noble/ed25519": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-1.7.3.tgz", @@ -7542,9 +8384,9 @@ "peer": true }, "@noble/hashes": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.0.tgz", - "integrity": "sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "peer": true }, "@noble/secp256k1": { @@ -7612,13 +8454,17 @@ "requires": { "@types/chai": "^4.3.4", "@types/jest": "^29.5.1", + "@types/rfdc": "^1.2.0", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", "chai": "^4.3.7", - "eslint": "^8.40.0", + "eslint": "^8.56.0", "jest": "^29.5.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", + "rfdc": "^1.4.1", "ts-jest": "^29.1.0", - "typescript": "^5.0.4" + "typescript": "^5.3.3" } }, "@race-foundation/sdk-facade": { @@ -7651,6 +8497,33 @@ "typescript": "^5.0.4" } }, + "@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "peer": true + }, + "@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "peer": true, + "requires": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, + "@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "peer": true, + "requires": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + } + }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -7732,104 +8605,113 @@ } }, "@swc/core": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.60.tgz", - "integrity": "sha512-dWfic7sVjnrStzGcMWakHd2XPau8UXGPmFUTkx6xGX+DOVtfAQVzG6ZW7ohw/yNcTqI05w6Ser26XMTMGBgXdA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", + "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", "dev": true, "peer": true, "requires": { - "@swc/core-darwin-arm64": "1.3.60", - "@swc/core-darwin-x64": "1.3.60", - "@swc/core-linux-arm-gnueabihf": "1.3.60", - "@swc/core-linux-arm64-gnu": "1.3.60", - "@swc/core-linux-arm64-musl": "1.3.60", - "@swc/core-linux-x64-gnu": "1.3.60", - "@swc/core-linux-x64-musl": "1.3.60", - "@swc/core-win32-arm64-msvc": "1.3.60", - "@swc/core-win32-ia32-msvc": "1.3.60", - "@swc/core-win32-x64-msvc": "1.3.60" + "@swc/core-darwin-arm64": "1.3.101", + "@swc/core-darwin-x64": "1.3.101", + "@swc/core-linux-arm-gnueabihf": "1.3.101", + "@swc/core-linux-arm64-gnu": "1.3.101", + "@swc/core-linux-arm64-musl": "1.3.101", + "@swc/core-linux-x64-gnu": "1.3.101", + "@swc/core-linux-x64-musl": "1.3.101", + "@swc/core-win32-arm64-msvc": "1.3.101", + "@swc/core-win32-ia32-msvc": "1.3.101", + "@swc/core-win32-x64-msvc": "1.3.101", + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" } }, "@swc/core-darwin-arm64": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.60.tgz", - "integrity": "sha512-oCDKWGdSO1WyErduGfiITRDoq7ZBt9PXETlhi8BGKH/wCc/3mfSNI9wXAg3Stn8mrT0lUJtdsnwMI/eZp6dK+A==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", + "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", "dev": true, "optional": true, "peer": true }, "@swc/core-darwin-x64": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.60.tgz", - "integrity": "sha512-pcE/1oUlmN/BkKndOPtViqTkaM5pomagXATo+Muqn4QNMnkSOEVcmF9T3Lr3nB1A7O/fwCew3/aHwZ5B2TZ1tA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", + "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", "dev": true, "optional": true, "peer": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.60.tgz", - "integrity": "sha512-Moc+86SWcbPr06PaQYUb0Iwli425F7QgjwTCNEPYA6OYUsjaJhXMaHViW2WdGIXue2+eaQbg31BHQd14jXcoBg==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", + "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", "dev": true, "optional": true, "peer": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.60.tgz", - "integrity": "sha512-pPGZrTgSXBvp6IrXPXz8UJr82AElf8hMuK4rNHmLGDCqrWnRIFLUpiAsc2WCFIgdwqitZNQoM+F2vbceA/bkKg==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", + "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", "dev": true, "optional": true, "peer": true }, "@swc/core-linux-arm64-musl": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.60.tgz", - "integrity": "sha512-HSFQaVUkjWYNsQeymAQ3IPX3csRQvHe6MFyqPfvCCQ4dFlxPvlS7VvNaLnGG+ZW1ek7Lc+hEX+4NGzZKsxDIHA==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", + "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", "dev": true, "optional": true, "peer": true }, "@swc/core-linux-x64-gnu": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.60.tgz", - "integrity": "sha512-WJt/X6HHM3/TszckRA7UKMXec3FHYsB9xswQbIYxN4bfTQodu3Rc8bmpHYtFO7ScMLrhY+RljHLK6wclPvaEXw==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", + "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", "dev": true, "optional": true, "peer": true }, "@swc/core-linux-x64-musl": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.60.tgz", - "integrity": "sha512-DGGBqAPUXy/aPMBKokL3osZC9kM97HchiDPuprzwgTMP40YQ3hGCzNJ5jK7sOk9Tc4PEdZ2Igfr9sBHmCrxxQw==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", + "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", "dev": true, "optional": true, "peer": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.60.tgz", - "integrity": "sha512-wQg/BZPJvp5WpUbsBp7VHjhUh0DfYOPhP6dH67WO9QQ07+DvOk2DR2Bfh0z0ts1k7H/FsAqExWtTDCWMCRJiRQ==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", + "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", "dev": true, "optional": true, "peer": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.60.tgz", - "integrity": "sha512-nqkd0XIVyGbnBwAxP4GIfx6n45/hAPETpmQYpDSGnucOKFJfvGdFGL81GDG1acPCq/oFtR3tIyTbPpKmJ0N6xQ==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", + "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", "dev": true, "optional": true, "peer": true }, "@swc/core-win32-x64-msvc": { - "version": "1.3.60", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.60.tgz", - "integrity": "sha512-ouw+s22i9PYQpSE7Xc+ZittEyA87jElXABesviSpP+jgHt10sM5KFUpVAeV8DRlxJCXMJJ5AhOdCf4TAtFr+6A==", + "version": "1.3.101", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", + "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", "dev": true, "optional": true, "peer": true }, + "@swc/counter": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", + "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", + "dev": true, + "peer": true + }, "@swc/register": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@swc/register/-/register-0.1.10.tgz", @@ -7841,6 +8723,13 @@ "source-map-support": "^0.5.13" } }, + "@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true, + "peer": true + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -7960,87 +8849,291 @@ "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", "dev": true, "requires": { - "@types/istanbul-lib-report": "*" + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", + "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + }, + "@types/node-fetch": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", + "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + }, + "dependencies": { + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, + "@types/prettier": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", + "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", + "dev": true + }, + "@types/rfdc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/rfdc/-/rfdc-1.2.0.tgz", + "integrity": "sha512-9Q81yCQwj4iuqDWVEcZMaY3QJ+w+Vj3M71ZdobgFC6T+RowLlE5CaLwRrwMiFvE3LZ7Qy1SDrR+EjGtVu9FmJQ==", + "requires": { + "rfdc": "*" + } + }, + "@types/semver": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.15.0.tgz", + "integrity": "sha512-j5qoikQqPccq9QoBAupOP+CBu8BaJ8BLjaXSioDISeTZkVO3ig7oSIKh3H+rEpee7xCXtWwSB4KIL5l6hWZzpg==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/type-utils": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/parser": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz", + "integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz", + "integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0" } }, - "@types/jest": { - "version": "29.5.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", - "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "@typescript-eslint/type-utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.15.0.tgz", + "integrity": "sha512-CnmHKTfX6450Bo49hPg2OkIm/D/TVYV7jO1MCfPYGwf6x3GO0VU8YMO5AYMn+u3X05lRRxA4fWCz87GFQV6yVQ==", "dev": true, "requires": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "@typescript-eslint/typescript-estree": "6.15.0", + "@typescript-eslint/utils": "6.15.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" } }, - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" + "@typescript-eslint/types": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz", + "integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==", + "dev": true }, - "@types/node-fetch": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.3.tgz", - "integrity": "sha512-ETTL1mOEdq/sxUtgtOhKjyB2Irra4cjxksvcMUR5Zr4n+PxVhsCD9WS46oPbHL3et9Zde7CNRr+WUNlcHvsX+w==", + "@typescript-eslint/typescript-estree": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz", + "integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==", "dev": true, "requires": { - "@types/node": "*", - "form-data": "^3.0.0" + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/visitor-keys": "6.15.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "dependencies": { - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "@types/prettier": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", - "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, - "@types/ws": { - "version": "7.4.7", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", - "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", - "peer": true, + "@typescript-eslint/utils": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.15.0.tgz", + "integrity": "sha512-eF82p0Wrrlt8fQSRL0bGXzK5nWPRV2dYQZdajcfzOD9+cQz9O7ugifrJxclB+xVOvWvagXfqS4Es7vpLP4augw==", + "dev": true, "requires": { - "@types/node": "*" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.15.0", + "@typescript-eslint/types": "6.15.0", + "@typescript-eslint/typescript-estree": "6.15.0", + "semver": "^7.5.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } } }, - "@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "@typescript-eslint/visitor-keys": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz", + "integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==", "dev": true, "requires": { - "@types/yargs-parser": "*" + "@typescript-eslint/types": "6.15.0", + "eslint-visitor-keys": "^3.4.1" } }, - "@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", "dev": true }, "acorn-jsx": { @@ -8138,6 +9231,12 @@ "is-array-buffer": "^3.0.1" } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -8362,6 +9461,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "buffer-reverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-reverse/-/buffer-reverse-1.0.1.tgz", + "integrity": "sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==", + "peer": true + }, "bufferutil": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", @@ -8531,6 +9636,12 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "peer": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8606,6 +9717,23 @@ "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -8734,27 +9862,28 @@ "dev": true }, "eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.40.0.tgz", - "integrity": "sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.4.0", - "@eslint/eslintrc": "^2.0.3", - "@eslint/js": "8.40.0", - "@humanwhocodes/config-array": "^0.11.8", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.10.0", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.0", - "eslint-visitor-keys": "^3.4.1", - "espree": "^9.5.2", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -8762,22 +9891,19 @@ "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", - "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" }, "dependencies": { @@ -8821,16 +9947,6 @@ "argparse": "^2.0.1" } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8840,20 +9956,6 @@ "p-locate": "^5.0.0" } }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -8863,21 +9965,6 @@ "p-limit": "^3.0.2" } }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -8887,9 +9974,9 @@ } }, "eslint-scope": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", - "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -8897,18 +9984,18 @@ } }, "eslint-visitor-keys": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", - "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, "espree": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", - "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "requires": { - "acorn": "^8.8.0", + "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } @@ -8949,6 +10036,53 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "ethereum-bloom-filters": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-bloom-filters/-/ethereum-bloom-filters-1.2.0.tgz", + "integrity": "sha512-28hyiE7HVsWubqhpVLVmZXFd4ITeHi+BUu05o9isf0GUpMtzBUi+8/gFrGaGYzvGAJQmJ3JKj77Mk9G98T84rA==", + "peer": true, + "requires": { + "@noble/hashes": "^1.4.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "peer": true + } + } + }, + "ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "peer": true, + "requires": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "ethjs-unit": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ethjs-unit/-/ethjs-unit-0.1.6.tgz", + "integrity": "sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==", + "peer": true, + "requires": { + "bn.js": "4.11.6", + "number-to-bn": "1.7.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "peer": true + } + } + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -9003,6 +10137,30 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -9225,6 +10383,20 @@ "define-properties": "^1.1.3" } }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -9240,10 +10412,10 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "has": { @@ -9484,6 +10656,12 @@ "is-extglob": "^2.1.1" } }, + "is-hex-prefixed": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", + "integrity": "sha512-WvtOiug1VFrE9v1Cydwm+FnXd3+w9GaeVUss5W4v/SLy3UW00vP+6iNF2SdnfiBoLy4bTqVdkftNGTUeOFVsbA==", + "peer": true + }, "is-negative-zero": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", @@ -10116,12 +11294,6 @@ } } }, - "js-sdsl": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", - "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10208,6 +11380,16 @@ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, "lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -10331,6 +11513,31 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "merkletreejs": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.4.0.tgz", + "integrity": "sha512-a48Ta5kWiVNBgeEbZVMm6FB1hBlp6vEuou/XnZdlkmd2zq6NZR6Sh2j+kR1B0iOZIXrTMcigBYzZ39MLdYhm1g==", + "peer": true, + "requires": { + "bignumber.js": "^9.0.1", + "buffer-reverse": "^1.0.1", + "crypto-js": "^4.2.0", + "treeify": "^1.1.0", + "web3-utils": "^1.3.4" + } + }, + "micro-ftch": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/micro-ftch/-/micro-ftch-0.3.1.tgz", + "integrity": "sha512-/0LLxhzP0tfiR5hcQebtudP56gUurs2CLkGarnCiB/OqEyUFQ6U3paQi/tgLv0hBJYt2rnr9MNpxz4fiiugstg==", + "peer": true + }, "micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -10575,6 +11782,24 @@ "path-key": "^3.0.0" } }, + "number-to-bn": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", + "integrity": "sha512-wsJ9gfSz1/s4ZsJN01lyonwuxA1tml6X1yBDnfpMglypcBRFZZkus26EdPSlqS5GJfYddVZa22p3VNb3z5m5Ig==", + "peer": true, + "requires": { + "bn.js": "4.11.6", + "strip-hex-prefix": "1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.6", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.6.tgz", + "integrity": "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==", + "peer": true + } + } + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -10617,6 +11842,20 @@ "mimic-fn": "^2.1.0" } }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10751,6 +11990,12 @@ "find-up": "^4.0.0" } }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, "prettier": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", @@ -10787,9 +12032,9 @@ } }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "pure-rand": { @@ -10804,6 +12049,15 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -10882,6 +12136,11 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11145,6 +12404,15 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-hex-prefix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", + "integrity": "sha512-q8d4ue7JGEiVcypji1bALTos+0pWtyGlivAWyPuTkHzuTCJqrK9sWxYQZUq6Nq3cuyv3bm734IhHvHtGGURU6A==", + "peer": true, + "requires": { + "is-hex-prefixed": "1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11228,6 +12496,19 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "peer": true }, + "treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "peer": true + }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-chacha20": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ts-chacha20/-/ts-chacha20-1.2.0.tgz", @@ -11305,6 +12586,15 @@ } } }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -11329,9 +12619,9 @@ } }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true }, "unbox-primitive": { @@ -11381,6 +12671,12 @@ "node-gyp-build": "^4.3.0" } }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "peer": true + }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -11431,6 +12727,22 @@ "makeerror": "1.0.12" } }, + "web3-utils": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.4.tgz", + "integrity": "sha512-tsu8FiKJLk2PzhDl9fXbGUWTkkVXYhtTA+SmEFkKft+9BgwLxfCRpU96sWv7ICC8zixBNd3JURVoiR3dUXgP8A==", + "peer": true, + "requires": { + "@ethereumjs/util": "^8.1.0", + "bn.js": "^5.2.1", + "ethereum-bloom-filters": "^1.0.6", + "ethereum-cryptography": "^2.1.2", + "ethjs-unit": "0.1.6", + "number-to-bn": "1.7.0", + "randombytes": "^2.1.0", + "utf8": "3.0.0" + } + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -11483,12 +12795,6 @@ "is-typed-array": "^1.1.10" } }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/js/sdk-core/.eslintrc.cjs b/js/sdk-core/.eslintrc.cjs new file mode 100644 index 00000000..c8102e3b --- /dev/null +++ b/js/sdk-core/.eslintrc.cjs @@ -0,0 +1,7 @@ +/* eslint-env node */ +module.exports = { + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + root: true, +}; diff --git a/js/sdk-core/package.json b/js/sdk-core/package.json index a0f5068c..00f63a10 100644 --- a/js/sdk-core/package.json +++ b/js/sdk-core/package.json @@ -3,6 +3,7 @@ "version": "0.2.6", "description": "The type definitions for Race SDK", "scripts": { + "check": "tsc -p tsconfig.types.json --skipLibCheck", "test": "jest", "test:watch": "jest --watch", "build": "run-p -l build:*", @@ -33,18 +34,22 @@ "devDependencies": { "@types/chai": "^4.3.4", "@types/jest": "^29.5.1", + "@typescript-eslint/eslint-plugin": "^6.15.0", + "@typescript-eslint/parser": "^6.15.0", + "@types/rfdc": "^1.2.0", "chai": "^4.3.7", - "eslint": "^8.40.0", + "eslint": "^8.56.0", "jest": "^29.5.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.7", "ts-jest": "^29.1.0", - "typescript": "^5.0.4" + "typescript": "^5.3.3" }, "peerDependencies": { "@race-foundation/borsh": "*", - "ts-chacha20": "^1.2.0" + "merkletreejs": "^0.4.0", + "ts-chacha20": "^1.2.0", + "rfdc": "^1.4.1" }, - "testDependencies": { - } + "testDependencies": {} } diff --git a/js/sdk-core/src/accounts.ts b/js/sdk-core/src/accounts.ts index ce0fb987..c50c081a 100644 --- a/js/sdk-core/src/accounts.ts +++ b/js/sdk-core/src/accounts.ts @@ -1,4 +1,5 @@ import { field, array, struct, option, enums, variant } from '@race-foundation/borsh'; +import { Checkpoint, CheckpointOnChain } from './checkpoint'; export interface IPlayerJoin { readonly addr: string; @@ -58,8 +59,7 @@ export interface IGameAccount { readonly data: Uint8Array; readonly entryType: EntryType; readonly recipientAddr: string; - readonly checkpoint: Uint8Array; - readonly checkpointAccessVersion: bigint; + readonly checkpointOnChain: CheckpointOnChain | undefined; } export interface IServerAccount { @@ -106,7 +106,17 @@ export class Token implements IToken { } } -export class TokenWithBalance implements IToken { +export interface ITokenWithBalance extends IToken { + readonly addr: string; + readonly icon: string; + readonly name: string; + readonly symbol: string; + readonly decimals: number; + readonly amount: bigint; + readonly uiAmount: string; +} + +export class TokenWithBalance implements ITokenWithBalance { readonly addr!: string; readonly icon!: string; readonly name!: string; @@ -148,13 +158,13 @@ export interface IRecipientSlot { readonly slotType: RecipientSlotType; readonly tokenAddr: string; readonly shares: IRecipientSlotShare[]; + readonly balance: bigint; } export interface IRecipientSlotShare { readonly owner: RecipientSlotOwner; readonly weights: number; readonly claimAmount: bigint; - readonly claimAmountCap: bigint; } export abstract class RecipientSlotOwner {} @@ -183,13 +193,18 @@ export type EntryTypeKind = | 'Invalid' | 'Cash' | 'Ticket' - | 'Gating'; + | 'Gating' + | 'Disabled'; export interface IEntryTypeKind { kind(): EntryTypeKind; } -export abstract class EntryType {} +export abstract class EntryType implements IEntryTypeKind { + kind(): EntryTypeKind { + return 'Invalid'; + } +} @variant(0) export class EntryTypeCash extends EntryType implements IEntryTypeKind { @@ -200,6 +215,7 @@ export class EntryTypeCash extends EntryType implements IEntryTypeKind { constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, EntryTypeCash.prototype); } kind(): EntryTypeKind { return 'Cash'; @@ -207,7 +223,7 @@ export class EntryTypeCash extends EntryType implements IEntryTypeKind { } @variant(1) -export class EntryTypeTicket extends EntryType implements IEntryTypeKind{ +export class EntryTypeTicket extends EntryType implements IEntryTypeKind { @field('u8') slotId!: number; @field('u64') @@ -215,6 +231,7 @@ export class EntryTypeTicket extends EntryType implements IEntryTypeKind{ constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, EntryTypeTicket.prototype); } kind(): EntryTypeKind { return 'Ticket'; @@ -222,18 +239,30 @@ export class EntryTypeTicket extends EntryType implements IEntryTypeKind{ } @variant(2) -export class EntryTypeGating extends EntryType implements IEntryTypeKind{ +export class EntryTypeGating extends EntryType implements IEntryTypeKind { @field('string') collection!: string; constructor(fields: any) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, EntryTypeGating.prototype); } kind(): EntryTypeKind { return 'Gating'; } } +@variant(3) +export class EntryTypeDisabled extends EntryType implements IEntryTypeKind { + constructor(_: any) { + super(); + Object.setPrototypeOf(this, EntryTypeDisabled.prototype); + } + kind(): EntryTypeKind { + return 'Disabled'; + } +} + export class Nft implements INft { @field('string') readonly addr!: string; @@ -352,10 +381,8 @@ export class GameAccount implements IGameAccount { readonly entryType!: EntryType; @field('string') readonly recipientAddr!: string; - @field('u8-array') - readonly checkpoint!: Uint8Array; - @field('u64') - readonly checkpointAccessVersion!: bigint; + @field(option(struct(CheckpointOnChain))) + readonly checkpointOnChain: CheckpointOnChain | undefined; constructor(fields: IGameAccount) { Object.assign(this, fields); } @@ -437,8 +464,6 @@ export class RecipientSlotShare implements IRecipientSlotShare { weights!: number; @field('u64') claimAmount!: bigint; - @field('u64') - claimAmountCap!: bigint; constructor(fields: IRecipientSlotShare) { Object.assign(this, fields); } @@ -453,6 +478,8 @@ export class RecipientSlot implements IRecipientSlot { tokenAddr!: string; @field(array(struct(RecipientSlotShare))) shares!: IRecipientSlotShare[]; + @field('u64') + balance!: bigint; constructor(fields: IRecipientSlot) { Object.assign(this, fields); } diff --git a/js/sdk-core/src/app-client.ts b/js/sdk-core/src/app-client.ts index 15567acf..ce0bcf1f 100644 --- a/js/sdk-core/src/app-client.ts +++ b/js/sdk-core/src/app-client.ts @@ -1,155 +1,124 @@ import { - BroadcastFrameEvent, - BroadcastFrameMessage, - BroadcastFrameTxState, Connection, - ConnectionState, + GetCheckpointParams, IConnection, - Message, - SubmitEventParams, - SubmitMessageParams, - SubscribeEventParams, } from './connection'; import { GameContext } from './game-context'; -import { GameContextSnapshot } from './game-context-snapshot'; import { ITransport, TransactionResult } from './transport'; import { IWallet } from './wallet'; -import { Handler, InitAccount } from './handler'; +import { Handler } from './handler'; import { Encryptor, IEncryptor } from './encryptor'; import { SdkError } from './error'; -import { EntryType, EntryTypeCash, GameAccount, GameBundle, IToken, PlayerProfile } from './accounts'; -import { TxState } from './tx-state'; import { Client } from './client'; -import { Custom, GameEvent, ICustomEvent } from './events'; -import { ProfileCache } from './profile-cache'; import { IStorage, getTtlCache, setTtlCache } from './storage'; import { DecryptionCache } from './decryption-cache'; +import { ProfileLoader } from './profile-loader'; +import { BaseClient } from './base-client'; +import { EntryTypeCash, GameAccount, GameBundle, IToken } from './accounts'; +import { + ConnectionStateCallbackFunction, + EventCallbackFunction, + GameInfo, + MessageCallbackFunction, + TxStateCallbackFunction, + PlayerProfileWithPfp, + ProfileCallbackFunction, + ErrorCallbackFunction +} from './types'; +import { SubClient } from './sub-client'; +import { Checkpoint } from './checkpoint'; +import { clone } from './utils'; const BUNDLE_CACHE_TTL = 3600 * 365; -export type EventCallbackFunction = ( - context: GameContextSnapshot, - state: Uint8Array, - event: GameEvent | undefined -) => void; -export type MessageCallbackFunction = (message: Message) => void; -export type TxStateCallbackFunction = (txState: TxState) => void; -export type OnConnectionStateCallbackFunction = (connState: ConnectionState) => void; - export type AppClientInitOpts = { transport: ITransport; wallet: IWallet; gameAddr: string; + onProfile: ProfileCallbackFunction; onEvent: EventCallbackFunction; onMessage?: MessageCallbackFunction; onTxState?: TxStateCallbackFunction; - onConnectionState?: OnConnectionStateCallbackFunction; + onError?: ErrorCallbackFunction; + onConnectionState?: ConnectionStateCallbackFunction; storage?: IStorage; }; +export type SubClientInitOpts = { + gameId: number; + gameAddr: string; + onEvent: EventCallbackFunction; + onMessage?: MessageCallbackFunction; + onTxState?: TxStateCallbackFunction; + onError?: ErrorCallbackFunction; + onConnectionState?: ConnectionStateCallbackFunction; +}; + export type JoinOpts = { amount: bigint; position?: number; createProfileIfNeeded?: boolean; }; -export type GameInfo = { - title: string; - maxPlayers: number; - minDeposit?: bigint; - maxDeposit?: bigint; - entryType: EntryType, - token: IToken; - tokenAddr: string; - bundleAddr: string; - data: Uint8Array; - dataLen: number; +export type AppClientCtorOpts = { + gameAddr: string; + gameAccount: GameAccount; + handler: Handler; + wallet: IWallet; + client: Client; + transport: ITransport; + connection: IConnection; + gameContext: GameContext; + onEvent: EventCallbackFunction; + onMessage: MessageCallbackFunction | undefined; + onTxState: TxStateCallbackFunction | undefined; + onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; + encryptor: IEncryptor; + info: GameInfo; + decryptionCache: DecryptionCache; + profileLoader: ProfileLoader; + storage: IStorage | undefined; + endpoint: string; }; -export class AppClient { - #gameAddr: string; - #handler: Handler; - #wallet: IWallet; - #client: Client; - #transport: ITransport; - #connection: IConnection; - #gameContext: GameContext; - #onEvent: EventCallbackFunction; - #onMessage?: MessageCallbackFunction; - #onTxState?: TxStateCallbackFunction; - #onConnectionState?: OnConnectionStateCallbackFunction; - #encryptor: IEncryptor; - #profileCaches: ProfileCache; - #info: GameInfo; - #decryptionCache: DecryptionCache; - - constructor( - gameAddr: string, - handler: Handler, - wallet: IWallet, - client: Client, - transport: ITransport, - connection: IConnection, - gameContext: GameContext, - onEvent: EventCallbackFunction, - onMessage: MessageCallbackFunction | undefined, - onTxState: TxStateCallbackFunction | undefined, - onConnectionState: OnConnectionStateCallbackFunction | undefined, - encryptor: IEncryptor, - info: GameInfo, - decryptionCache: DecryptionCache, - ) { - this.#gameAddr = gameAddr; - this.#handler = handler; - this.#wallet = wallet; - this.#client = client; - this.#transport = transport; - this.#connection = connection; - this.#gameContext = gameContext; - this.#onEvent = onEvent; - this.#onMessage = onMessage; - this.#onTxState = onTxState; - this.#onConnectionState = onConnectionState; - this.#encryptor = encryptor; - this.#profileCaches = new ProfileCache(transport); - this.#info = info; - this.#decryptionCache = decryptionCache; +export class AppClient extends BaseClient { + __profileLoader: ProfileLoader; + __storage?: IStorage; + __endpoint: string; + __latestGameAccount: GameAccount; + + constructor(opts: AppClientCtorOpts) { + super({ + onLoadProfile: (id, addr) => opts.profileLoader.load(id, addr), + logPrefix: 'MainGame|', + gameId: 0, + latestCheckpointOnChain: opts.gameAccount.checkpointOnChain, + ...opts + }); + this.__profileLoader = opts.profileLoader; + this.__storage = opts.storage; + this.__endpoint = opts.endpoint; + this.__latestGameAccount = opts.gameAccount; } static async initialize(opts: AppClientInitOpts): Promise { - const { transport, wallet, gameAddr, onEvent, onMessage, onTxState, onConnectionState, storage } = opts; - console.group('AppClient initialization'); + const { transport, wallet, gameAddr, onEvent, onMessage, onTxState, onConnectionState, onError, onProfile, storage } = opts; + + console.group(`Initialize AppClient, gameAddr = ${gameAddr}`); try { const playerAddr = wallet.walletAddr; + console.log(`PlayerAddr = ${playerAddr}`); const encryptor = await Encryptor.create(playerAddr, storage); const gameAccount = await transport.getGameAccount(gameAddr); + console.log('Game Account:', gameAccount); if (gameAccount === undefined) { throw SdkError.gameAccountNotFound(gameAddr); } - console.log('Game account:', gameAccount); - // Fetch game bundle - // The bundle can be considered as immutable, so we use cache whenever possible const bundleCacheKey = `BUNDLE__${transport.chain}_${gameAccount.bundleAddr}`; - let gameBundle: GameBundle | undefined; - if (storage !== undefined) { - gameBundle = getTtlCache(storage, bundleCacheKey); - console.log('Use game bundle from cache:', gameBundle); - if (gameBundle !== undefined) { - Object.assign(gameBundle, { data: Uint8Array.of() }) - } - } - if (gameBundle === undefined) { - gameBundle = await transport.getGameBundle(gameAccount.bundleAddr); - console.log('Game bundle:', gameBundle); - if (gameBundle !== undefined && storage !== undefined && gameBundle.data.length === 0) { - setTtlCache(storage, bundleCacheKey, gameBundle, BUNDLE_CACHE_TTL); - } - } - if (gameBundle === undefined) { - throw SdkError.gameBundleNotFound(gameAccount.bundleAddr); - } + const gameBundle = await getGameBundle(transport, storage, bundleCacheKey, gameAccount.bundleAddr); const transactorAddr = gameAccount.transactorAddr; if (transactorAddr === undefined) { @@ -161,34 +130,37 @@ export class AppClient { throw SdkError.transactorAccountNotFound(transactorAddr); } const decryptionCache = new DecryptionCache(); - console.log('Transactor endpoint:', transactorAccount.endpoint); - const connection = Connection.initialize(gameAddr, playerAddr, transactorAccount.endpoint, encryptor); - const client = new Client(playerAddr, gameAddr, encryptor, connection); + const endpoint = transactorAccount.endpoint; + console.log('Transactor endpoint:', endpoint); + const connection = Connection.initialize(gameAddr, playerAddr, endpoint, encryptor); + const client = new Client(playerAddr, encryptor, connection); const handler = await Handler.initialize(gameBundle, encryptor, client, decryptionCache); - const gameContext = new GameContext(gameAccount); - gameContext.applyCheckpoint(gameContext.checkpointAccessVersion, gameContext.settleVersion); + + const getCheckpointParams = new GetCheckpointParams({ settleVersion: gameAccount.settleVersion }); + const checkpointOffChain = await connection.getCheckpoint(getCheckpointParams); + + console.log('Get checkpoint onchain from game account:', gameAccount.checkpointOnChain); + console.log('Get checkpoint offchain from transactor:', checkpointOffChain); + let checkpoint; + if (checkpointOffChain !== undefined && gameAccount.checkpointOnChain !== undefined) { + checkpoint = Checkpoint.fromParts(checkpointOffChain, gameAccount.checkpointOnChain); + } else { + checkpoint = Checkpoint.default(); + } + + const gameContext = new GameContext(gameAccount, checkpoint); + console.log('Game Context:', clone(gameContext)); const token = await transport.getToken(gameAccount.tokenAddr); if (token === undefined) { throw SdkError.tokenNotFound(gameAccount.tokenAddr); } - const info: GameInfo = { - title: gameAccount.title, - entryType: gameAccount.entryType, - maxPlayers: gameAccount.maxPlayers, - tokenAddr: gameAccount.tokenAddr, - bundleAddr: gameAccount.bundleAddr, - data: gameAccount.data, - dataLen: gameAccount.dataLen, - token, - }; - - if (gameAccount.entryType instanceof EntryTypeCash) { - info.minDeposit = gameAccount.entryType.minDeposit; - info.maxDeposit = gameAccount.entryType.maxDeposit; - } + const info = makeGameInfo(gameAccount, token); + const profileLoader = new ProfileLoader(transport, storage, onProfile); + profileLoader.start(); - return new AppClient( + return new AppClient({ gameAddr, + gameAccount, handler, wallet, client, @@ -199,253 +171,190 @@ export class AppClient { onMessage, onTxState, onConnectionState, + onError, encryptor, info, decryptionCache, - ); + profileLoader, + storage, + endpoint, + }); } finally { console.groupEnd(); } } - get playerAddr() { - return this.#wallet.walletAddr; - } + async subClient(opts: SubClientInitOpts): Promise { + try { + const { gameId, onEvent, onMessage, onTxState, onConnectionState, onError } = opts; - get gameAddr() { - return this.#gameAddr; - } + const addr = `${this.__gameAddr}:${gameId.toString()}`; - get gameContext(): GameContext { - return this.#gameContext; - } + console.group(`SubClient initialization, id: ${gameId}`); + console.log('Parent Game Context:', clone(this.__gameContext)); - /** - * Get player profile by its wallet address. - */ - async getProfile(addr: string): Promise { - return await this.#profileCaches.getProfile(addr); - } + // Query the on-chain game account to get the latest checkpoint. + const gameAccount = await this.__getGameAccount(); + const checkpointOnChain = gameAccount.checkpointOnChain; + const settleVersion = gameAccount.settleVersion; - async invokeEventCallback(event: GameEvent | undefined) { - const snapshot = new GameContextSnapshot(this.#gameContext); - await this.#profileCaches.injectProfiles(snapshot); - const state = this.#gameContext.handlerState; - this.#onEvent(snapshot, state, event); - } + const subGame = this.__gameContext.findSubGame(gameId); - async __initializeState(gameAccount: GameAccount): Promise { - const initAccount = InitAccount.createFromGameAccount(gameAccount, this.gameContext.accessVersion, this.gameContext.settleVersion); - console.log('Initialize state with', initAccount); - await this.#handler.initState(this.#gameContext, initAccount); - await this.invokeEventCallback(undefined); - } + if (subGame === undefined) { + console.warn('Game context:', this.__gameContext); + throw SdkError.invalidSubId(gameId); + } else { + console.log('Sub Game:', subGame); + } + + const bundleAddr = subGame.bundleAddr; + + const bundleCacheKey = `BUNDLE__${this.__transport.chain}_${bundleAddr}`; - async __getGameAccount(): Promise { - while (true) { - try { - const gameAccount = await this.#transport.getGameAccount(this.gameAddr); - if (gameAccount === undefined) continue; - console.log('Game account', gameAccount); - return gameAccount; - } catch (e: any) { - console.warn(e, 'Failed to fetch game account, will retry in 3s'); - await new Promise(r => setTimeout(r, 3000)); - continue; + const decryptionCache = new DecryptionCache(); + const playerAddr = this.__wallet.walletAddr; + const connection = Connection.initialize(addr, playerAddr, this.__endpoint, this.__encryptor); + const client = new Client(playerAddr, this.__encryptor, connection); + const gameBundle = await getGameBundle(this.__transport, this.__storage, bundleCacheKey, bundleAddr); + const handler = await Handler.initialize(gameBundle, this.__encryptor, client, decryptionCache); + + // Reload the latest checkpoint. + const checkpointOffChain = await this.__connection.getCheckpoint(new GetCheckpointParams({ settleVersion })); + if (checkpointOffChain !== undefined && checkpointOnChain !== undefined) { + this.__gameContext.checkpoint = Checkpoint.fromParts(checkpointOffChain, checkpointOnChain); + } else { + this.__gameContext.checkpoint = Checkpoint.default(); } - } + const gameContext = this.__gameContext.subContext(subGame); + console.log("SubGame's GameContext:", clone(gameContext)); + + return new SubClient({ + gameAddr: addr, + wallet: this.__wallet, + transport: this.__transport, + encryptor: this.__encryptor, + latestCheckpointOnChain: checkpointOnChain, + onEvent, + onMessage, + onTxState, + onConnectionState, + onError, + handler, + connection, + client, + info: this.__info, + decryptionCache, + gameContext, + gameId, + }); + } finally { + console.groupEnd(); + } } /** * Connect to the transactor and retrieve the event stream. */ async attachGame() { - await this.#client.attachGame(); - const sub = this.#connection.subscribeEvents(); - const gameAccount = await this.__getGameAccount(); - this.#gameContext = new GameContext(gameAccount); - this.#gameContext.applyCheckpoint(gameAccount.checkpointAccessVersion, this.#gameContext.settleVersion); - await this.#connection.connect(new SubscribeEventParams({ settleVersion: this.#gameContext.settleVersion })); - await this.__initializeState(gameAccount); - - for await (const frame of sub) { - if (frame instanceof BroadcastFrameMessage) { - console.group('Receive message'); - try { - if (this.#onMessage !== undefined) { - const { message } = frame; - this.#onMessage(message); - } - } finally { - console.groupEnd(); - } - } else if (frame instanceof BroadcastFrameTxState) { - console.group('Receive tx state'); - try { - if (this.#onTxState !== undefined) { - const { txState } = frame; - this.#onTxState(txState); - } - } finally { - console.groupEnd(); - } - } else if (frame instanceof BroadcastFrameEvent) { - const { event, timestamp } = frame; - console.group('Handle event: ' + event.kind() + ' at timestamp: ' + new Date(Number(timestamp)).toLocaleString()); - console.log('Event: ', event); - try { - this.#gameContext.prepareForNextEvent(timestamp); - try { - let context = new GameContext(this.#gameContext); - await this.#handler.handleEvent(context, event); - this.#gameContext = context; - } catch (err: any) { - console.error(err); - } - await this.invokeEventCallback(event); - } catch (e: any) { - console.log("Game context in error:", this.#gameContext); - throw e; - } finally { - console.groupEnd(); - } - } else if (frame === 'disconnected') { - if (this.#onConnectionState !== undefined) { - this.#onConnectionState('disconnected') - } - console.group('Disconnected, try reset state and context'); - try { - const gameAccount = await this.__getGameAccount(); - this.#gameContext = new GameContext(gameAccount); - this.#gameContext.applyCheckpoint(gameAccount.checkpointAccessVersion, this.#gameContext.settleVersion); - await this.#connection.connect(new SubscribeEventParams({ settleVersion: this.#gameContext.settleVersion })); - await this.__initializeState(gameAccount); - } finally { - console.groupEnd(); - } - } else if (frame === 'connected') { - if (this.#onConnectionState !== undefined) { - this.#onConnectionState('connected') - } - } else if (frame === 'closed') { - if (this.#onConnectionState !== undefined) { - this.#onConnectionState('closed') - } - } else if (frame === 'reconnected') { - if (this.#onConnectionState !== undefined) { - this.#onConnectionState('reconnected') - } - } else { - console.log('Subscribe stream ended') - break; + console.group('Attach to game'); + let sub; + try { + await this.__attachGameWithRetry(); + sub = this.__connection.subscribeEvents(); + await this.__startSubscribe(); + for (const p of this.__latestGameAccount.players) { + this.__onLoadProfile(p.accessVersion, p.addr); } + } catch (e) { + console.error(this.__logPrefix + 'Attaching game failed', e); + this.__invokeErrorCallback('attach-failed') + throw e; + } finally { + console.groupEnd(); } + if (sub !== undefined) await this.__processSubscription(sub); } /** - * Join game. + * Get player profile by its wallet address. */ - async join(params: JoinOpts): Promise> { - const gameAccount = await this.#transport.getGameAccount(this.gameAddr); - if (gameAccount === undefined) { - throw new Error('Game account not found'); - } - const playersCount = gameAccount.players.length; - if (gameAccount.maxPlayers <= playersCount) { - throw new Error('Game is full'); - } - let position: number | undefined = params.position; - if (position === undefined) { - for (let i = 0; i < gameAccount.maxPlayers; i++) { - if (gameAccount.players.find(p => p.position === i) === undefined) { - position = i; - break; - } - } - } - if (position === undefined) { - throw new Error('Game is full'); + getProfile(id: bigint): Promise + getProfile(addr: string): Promise + async getProfile(idOrAddr: string | bigint): Promise { + let addr: string = '' + if (typeof idOrAddr === 'bigint') { + addr = this.__gameContext.idToAddr(idOrAddr); + } else { + addr = idOrAddr } + return this.__profileLoader.getProfile(addr); + } - const publicKey = await this.#encryptor.exportPublicKey(); + makeSubGameAddr(gameId: number): string { + return `${this.__gameAddr}:${gameId}`; + } - let createProfile = false; - if (params.createProfileIfNeeded) { - const p = await this.getProfile(this.playerAddr); - if (p === undefined) { - createProfile = true; - console.log('No profile account found, will create a new one.') - } - } + /** + * Join game. + */ + async join(params: JoinOpts): Promise> { + const publicKey = await this.__encryptor.exportPublicKey(); - return await this.#transport.join(this.#wallet, { + return await this.__transport.join(this.__wallet, { gameAddr: this.gameAddr, amount: params.amount, - accessVersion: gameAccount.accessVersion, - position, + position: params.position || 0, verifyKey: publicKey.ec, - createProfile, + createProfileIfNeeded: params.createProfileIfNeeded, }); } - /** - * Submit an event. - */ - submitEvent(raw: Uint8Array): Promise; - submitEvent(customEvent: ICustomEvent): Promise; - async submitEvent(arg: ICustomEvent | Uint8Array): Promise { - let raw = arg instanceof Uint8Array ? arg : arg.serialize(); - const event = new Custom({ sender: this.playerAddr, raw }); - const connState = await this.#connection.submitEvent( - new SubmitEventParams({ - event, - }) - ); - if (connState !== undefined && this.#onConnectionState !== undefined) { - this.#onConnectionState(connState); +} + + +// Miscellaneous + +export async function getGameBundle(transport: ITransport, storage: IStorage | undefined, bundleCacheKey: string, bundleAddr: string): Promise { + let gameBundle: GameBundle | undefined; + if (storage !== undefined) { + gameBundle = getTtlCache(storage, bundleCacheKey); + console.log('Use game bundle from cache:', gameBundle); + if (gameBundle !== undefined) { + Object.assign(gameBundle, { data: Uint8Array.of() }) } } - - /** - * Submit a message, contains arbitrary content. - */ - async submitMessage(message: string) { - const connState = await this.#connection.submitMessage( - new SubmitMessageParams({ - content: message, - }) - ); - if (connState !== undefined && this.#onConnectionState !== undefined) { - this.#onConnectionState(connState); + if (gameBundle === undefined) { + gameBundle = await transport.getGameBundle(bundleAddr); + console.log('Game bundle:', gameBundle); + if (gameBundle !== undefined && storage !== undefined && gameBundle.data.length === 0) { + setTtlCache(storage, bundleCacheKey, gameBundle, BUNDLE_CACHE_TTL); } } - - /** - * Get hidden knowledge by random id. The result contains both - * public and private information. For performance reason, it's - * better to cache the result somewhere instead of calling this - * function frequently. - */ - async getRevealed(randomId: number): Promise> { - return this.#decryptionCache.get(randomId) || new Map(); + if (gameBundle === undefined) { + throw SdkError.gameBundleNotFound(bundleAddr); } + return gameBundle; +} - /** - * Close current event subscription. - */ - async close() {} - /** - * Exit current game. - */ - async exit(): Promise; - async exit(keepConnection: boolean): Promise; - async exit(keepConnection: boolean = false) { - await this.#connection.exitGame({ keepConnection }); +export function makeGameInfo(gameAccount: GameAccount, token: IToken): GameInfo { + const info: GameInfo = { + gameAddr: gameAccount.addr, + title: gameAccount.title, + entryType: gameAccount.entryType, + maxPlayers: gameAccount.maxPlayers, + tokenAddr: gameAccount.tokenAddr, + bundleAddr: gameAccount.bundleAddr, + data: gameAccount.data, + dataLen: gameAccount.dataLen, + token, + }; + + if (gameAccount.entryType instanceof EntryTypeCash) { + info.minDeposit = gameAccount.entryType.minDeposit; + info.maxDeposit = gameAccount.entryType.maxDeposit; } - get info(): GameInfo { - return this.#info; - } + return info; } diff --git a/js/sdk-core/src/app-helper.ts b/js/sdk-core/src/app-helper.ts index 518b3715..c445b344 100644 --- a/js/sdk-core/src/app-helper.ts +++ b/js/sdk-core/src/app-helper.ts @@ -1,6 +1,7 @@ -import { EntryTypeCash, GameAccount, INft, IToken, PlayerProfile, TokenWithBalance } from './accounts'; +import { EntryTypeCash, GameAccount, INft, IToken, ITokenWithBalance, PlayerProfile, RecipientAccount, TokenWithBalance } from './accounts'; import { IStorage } from './storage'; import { CreateGameAccountParams, ITransport, TransactionResult } from './transport'; +import { PlayerProfileWithPfp } from './types'; import { IWallet } from './wallet'; @@ -9,6 +10,11 @@ export type AppHelperInitOpts = { storage?: IStorage, }; +export type ClaimPreview = { + tokenAddr: string, + amount: bigint, +}; + /** * The helper for common interaction. * @@ -118,8 +124,15 @@ export class AppHelper { * @param addr - The address of player profile account * @returns The player profile account or undefined when not found */ - async getProfile(addr: string): Promise { - return await this.#transport.getPlayerProfile(addr); + async getProfile(addr: string): Promise { + const profile = await this.#transport.getPlayerProfile(addr); + if (profile === undefined) return undefined; + if (profile.pfp !== undefined) { + const pfp = await this.#transport.getNft(profile.pfp, this.#storage); + return { nick: profile.nick, addr: profile.addr, pfp }; + } else { + return { nick: profile.nick, addr: profile.addr, pfp: undefined }; + } } /** @@ -142,12 +155,21 @@ export class AppHelper { } /** - * List available tokens. + * List tokens. * * @return A list of token info. */ - async listTokens(): Promise { - return await this.#transport.listTokens(this.#storage); + async listTokens(tokenAddrs: string[]): Promise { + return await this.#transport.listTokens(tokenAddrs, this.#storage); + } + + /** + * List tokens with their balance. + * + * @return A list of token info. + */ + async listTokensWithBalance(walletAddr: string, tokenAddrs: string[]): Promise { + return await this.#transport.listTokensWithBalance(walletAddr, tokenAddrs, this.#storage); } /** @@ -177,22 +199,61 @@ export class AppHelper { } /** - * Fetch tokens and balances + * Claim the fees collected by game. * - * @param walletAddr - The player's wallet address + * @param wallet - The wallet adapter to sign the transaction + * @param gameAddr - The address of game account. + */ + async claim(wallet: IWallet, gameAddr: string): Promise> { + const gameAccount = await this.#transport.getGameAccount(gameAddr); + if (gameAccount === undefined) throw new Error('Game account not found'); + return await this.#transport.recipientClaim(wallet, { recipientAddr: gameAccount?.recipientAddr }); + } + + async getRecipient(recipientAddr: string): Promise { + return await this.#transport.getRecipient(recipientAddr); + } + + /** + * Preview the claim information. * - * @return The list of tokens with `amount` and `uiAmount` added. + * @param wallet - The wallet adapter to sign the transaction + * @param recipientAddr | recipientAccount - The address of a recipient account. */ - async listTokensWithBalance(walletAddr: string): Promise { - const tokens = await this.listTokens(); - const tokenAddrs = tokens.map(t => t.addr); - const balanceMap = await this.#transport.fetchBalances(walletAddr, tokenAddrs); - return tokens.map(t => { - let balance = balanceMap.get(t.addr); - if (balance === undefined) { - balance = 0n; + previewClaim(wallet: IWallet, recipientAddr: string): Promise + previewClaim(wallet: IWallet, recipientAccount: RecipientAccount): Promise + async previewClaim(wallet: IWallet, recipient: RecipientAccount | string): Promise { + if (typeof recipient === 'string') { + const r = await this.#transport.getRecipient(recipient); + if (r === undefined) { + throw new Error('Recipient account not found'); } - return new TokenWithBalance(t, balance); - }); + recipient = r; + } + + let ret: ClaimPreview[] = []; + for (const slot of recipient.slots) { + let weights = 0; + let totalWeights = 0; + let totalClaimed = 0n; + let claimed = 0n; + for (const share of slot.shares) { + totalClaimed += share.claimAmount; + totalWeights += share.weights; + if (share.owner === wallet.walletAddr) { + weights += share.weights; + claimed += share.claimAmount; + } + } + const totalAmount = totalClaimed + slot.balance; + const amountToClaim = BigInt(Number(totalAmount) * weights / totalWeights) - claimed; + if (amountToClaim > 0n) { + ret.push({ + amount: amountToClaim, + tokenAddr: slot.tokenAddr + }); + } + } + return ret; } } diff --git a/js/sdk-core/src/base-client.ts b/js/sdk-core/src/base-client.ts new file mode 100644 index 00000000..d8ada0f3 --- /dev/null +++ b/js/sdk-core/src/base-client.ts @@ -0,0 +1,405 @@ +import { + ConnectionState, + IConnection, + SubmitEventParams, + SubscribeEventParams, + ConnectionSubscription, + SubmitMessageParams, +} from './connection'; +import { EventEffects, GameContext } from './game-context'; +import { GameContextSnapshot } from './game-context-snapshot'; +import { ITransport } from './transport'; +import { IWallet } from './wallet'; +import { Handler } from './handler'; +import { IEncryptor, sha256, sha256String } from './encryptor'; +import { GameAccount } from './accounts'; +import { PlayerConfirming } from './tx-state'; +import { Client } from './client'; +import { CheckpointReady, Custom, EndOfHistory, GameEvent, ICustomEvent, Init } from './events'; +import { DecryptionCache } from './decryption-cache'; +import { ConnectionStateCallbackFunction, ErrorCallbackFunction, ErrorKind, EventCallbackFunction, GameInfo, LoadProfileCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction } from './types'; +import { BroadcastFrame, BroadcastFrameEventHistories, BroadcastFrameMessage, BroadcastFrameSync, BroadcastFrameEvent, BroadcastFrameTxState } from './broadcast-frames'; +import { IInitAccount, InitAccount } from './init-account'; +import { Checkpoint, CheckpointOnChain } from './checkpoint'; +import { clone } from './utils'; + +const MAX_RETRIES = 3; + +export type InitState = { + initAccount: InitAccount, + checkpointOnChain: CheckpointOnChain | undefined, +}; + +export type BaseClientCtorOpts = { + gameAddr: string; + gameId: number; + handler: Handler; + wallet: IWallet; + client: Client; + transport: ITransport; + connection: IConnection; + gameContext: GameContext; + latestCheckpointOnChain: CheckpointOnChain | undefined; + onEvent: EventCallbackFunction; + onMessage: MessageCallbackFunction | undefined; + onTxState: TxStateCallbackFunction | undefined; + onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; + onLoadProfile: LoadProfileCallbackFunction; + encryptor: IEncryptor; + info: GameInfo; + decryptionCache: DecryptionCache; + logPrefix: string; +}; + +export class BaseClient { + __gameAddr: string; + __gameId: number; + __handler: Handler; + __wallet: IWallet; + __client: Client; + __transport: ITransport; + __connection: IConnection; + __gameContext: GameContext; + __onEvent: EventCallbackFunction; + __onMessage?: MessageCallbackFunction; + __onTxState?: TxStateCallbackFunction; + __onError?: ErrorCallbackFunction; + __onConnectionState?: ConnectionStateCallbackFunction; + __onLoadProfile: LoadProfileCallbackFunction; + __encryptor: IEncryptor; + __info: GameInfo; + __decryptionCache: DecryptionCache; + __logPrefix: string; + __latestCheckpointOnChain: CheckpointOnChain | undefined; + + constructor(opts: BaseClientCtorOpts) { + this.__gameAddr = opts.gameAddr; + this.__gameId = opts.gameId; + this.__latestCheckpointOnChain = opts.latestCheckpointOnChain; + this.__handler = opts.handler; + this.__wallet = opts.wallet; + this.__client = opts.client; + this.__transport = opts.transport; + this.__connection = opts.connection; + this.__gameContext = opts.gameContext; + this.__onEvent = opts.onEvent; + this.__onMessage = opts.onMessage; + this.__onTxState = opts.onTxState; + this.__onError = opts.onError; + this.__onConnectionState = opts.onConnectionState; + this.__encryptor = opts.encryptor; + this.__info = opts.info; + this.__decryptionCache = opts.decryptionCache; + this.__onLoadProfile = opts.onLoadProfile; + this.__logPrefix = opts.logPrefix; + } + + get playerAddr(): string { + return this.__wallet.walletAddr; + } + + get playerId(): bigint | undefined { + return this.__gameContext.addrToId(this.__wallet.walletAddr); + } + + get gameAddr(): string { + return this.__gameAddr; + } + + get gameContext(): GameContext { + return this.__gameContext; + } + + get info(): GameInfo { + return this.__info; + } + + /** + * Get hidden knowledge by random id. The result contains both + * public and private information. For performance reason, it's + * better to cache the result somewhere instead of calling this + * function frequently. + */ + async getRevealed(randomId: number): Promise> { + return this.__decryptionCache.get(randomId) || new Map(); + } + + /** + * Exit current game. + */ + async exit(): Promise; + async exit(keepConnection: boolean): Promise; + async exit(keepConnection: boolean = false) { + await this.__connection.exitGame({ keepConnection }); + } + + /** + * Detach the game connection. + */ + detach() { + this.__connection.disconnect(); + } + + /** + * Parse the id to player's address. + * + * Throw an error when it fails. + */ + idToAddr(id: bigint): string { + return this.__gameContext.idToAddr(id); + } + + /** + * Parse the player's address to its id. + * + * Throw an error when it fails. + */ + addrToId(addr: string): bigint { + return this.__gameContext.addrToId(addr); + } + + /** + * Submit an event. + */ + submitEvent(raw: Uint8Array): Promise; + submitEvent(customEvent: ICustomEvent): Promise; + async submitEvent(arg: ICustomEvent | Uint8Array): Promise { + let raw = arg instanceof Uint8Array ? arg : arg.serialize(); + const id = this.__gameContext.addrToId(this.playerAddr); + const event = new Custom({ sender: id, raw }); + const connState = await this.__connection.submitEvent( + new SubmitEventParams({ + event, + }) + ); + if (connState !== undefined && this.__onConnectionState !== undefined) { + this.__onConnectionState(connState); + } + } + + /** + * Submit a message. + */ + async submitMessage(content: string): Promise { + const connState = await this.__connection.submitMessage(new SubmitMessageParams({ + content + })); + if (connState !== undefined && this.__onConnectionState !== undefined) { + this.__onConnectionState(connState); + } + } + + async __attachGameWithRetry() { + for (let i = 0; i < 10; i++) { + const resp = await this.__client.attachGame(); + console.log('Attach response:', resp); + if (resp === 'success') { + break; + } else { + console.log(this.__logPrefix + 'Game is not ready, try again after 2 second.'); + await new Promise(r => setTimeout(r, 2000)); + } + } + + } + + __invokeErrorCallback(err: ErrorKind, arg?: any) { + if (this.__onError) { + this.__onError(err, arg) + } else { + console.error(`${this.__logPrefix}An error occured: ${err}, to handle it, use \`onError\`.`) + } + } + + async __invokeEventCallback(event: GameEvent | undefined) { + const snapshot = new GameContextSnapshot(this.__gameContext); + const state = this.__gameContext.handlerState; + console.log('Dispatch event callback for ', event?.kind()); + this.__onEvent(snapshot, state, event); + } + + async __getGameAccount(): Promise { + let retries = 0; + while (true) { + if (retries === MAX_RETRIES) { + this.__invokeErrorCallback('onchain-data-not-found') + throw new Error(`Game account not found, after ${retries} retries`); + } + try { + const gameAccount = await this.__transport.getGameAccount(this.gameAddr); + if (gameAccount === undefined) { + retries += 1; + continue; + } + return gameAccount; + } catch (e: any) { + console.warn(e, 'Failed to fetch game account, will retry in 3s'); + await new Promise(r => setTimeout(r, 3000)); + retries += 1; + continue; + } + } + } + + async __processSubscription(sub: ConnectionSubscription) { + for await (const item of sub) { + if (item === undefined) { + break; + } else if (item instanceof BroadcastFrame) { + await this.__handleBroadcastFrame(item); + } else { + await this.__handleConnectionState(item); + } + } + } + + + + async __checkStateSha(stateSha: string, err: ErrorKind) { + const sha = await sha256String(this.__gameContext.handlerState) + if (sha !== stateSha && stateSha !== '') { + console.warn(`An error occurred in event loop: ${err}, game: ${this.__gameAddr}, local: ${sha}, remote: ${stateSha}`); + } else { + console.log('State SHA validation passed:', stateSha); + } + } + + async __handleEvent(event: GameEvent, timestamp: bigint, stateSha: string) { + console.group(this.__logPrefix + 'Handle event: ' + event.kind() + ' at timestamp: ' + timestamp); + console.log('Event: ', event); + console.log('Game Context before:', clone(this.__gameContext)); + let state: Uint8Array | undefined; + let err: ErrorKind | undefined; + let effects: EventEffects | undefined; + + try { // For log group + try { + this.__gameContext.setTimestamp(timestamp); + effects = await this.__handler.handleEvent(this.__gameContext, event); + state = this.__gameContext.handlerState; + + await this.__checkStateSha(stateSha, 'event-state-sha-mismatch'); + } catch (e: any) { + console.error(this.__logPrefix, e); + err = 'handle-event-error'; + } + + if (!err) { + await this.__invokeEventCallback(event); + } + + // When there's a checkpoint, emit an event to indicate that. + if ((!err) && effects?.checkpoint) { + this.__invokeEventCallback(new CheckpointReady()); + } + + if (err) { + this.__invokeErrorCallback(err, state); + throw new Error(`An error occurred in event loop: ${err}`); + } + + } finally { + console.log('Game Context after:', clone(this.__gameContext)); + console.groupEnd() + } + } + + async __handleBroadcastFrame(frame: BroadcastFrame) { + if (frame instanceof BroadcastFrameMessage) { + console.group(`${this.__logPrefix}Receive message broadcast`); + try { + if (this.__onMessage !== undefined) { + const { message } = frame; + console.log('Message:', message); + this.__onMessage(message); + } + } finally { + console.groupEnd(); + } + } else if (frame instanceof BroadcastFrameTxState) { + console.group(`${this.__logPrefix}Receive transaction state broadcast`); + try { + if (this.__onTxState !== undefined) { + const { txState } = frame; + console.log('TxState:', txState); + if (txState instanceof PlayerConfirming) { + txState.confirmPlayers.forEach(p => { + this.__onLoadProfile(p.id, p.addr); + }); + } + this.__onTxState(txState); + } + } finally { + console.groupEnd(); + } + } else if (frame instanceof BroadcastFrameSync) { + console.group(`${this.__logPrefix}Receive sync broadcast`); + try { + console.log('Sync:', frame); + for (const node of frame.newServers) { + this.__gameContext.addNode(node.addr, node.accessVersion, + node.addr === frame.transactor_addr ? 'transactor' : 'validator'); + } + for (const node of frame.newPlayers) { + this.__gameContext.addNode(node.addr, node.accessVersion, 'player'); + this.__onLoadProfile(node.accessVersion, node.addr); + } + this.__gameContext.setAccessVersion(frame.accessVersion); + } finally { + console.groupEnd(); + } + } else if (frame instanceof BroadcastFrameEvent) { + const { event, timestamp, stateSha } = frame; + await this.__handleEvent(event, timestamp, stateSha); + } else if (frame instanceof BroadcastFrameEventHistories) { + console.group(`${this.__logPrefix}Receive event histories`); + try { + console.log('Frame:', frame); + console.log('Game context before:', clone(this.__gameContext)); + await this.__handler.initState(this.__gameContext); + await this.__checkStateSha(frame.stateSha, 'checkpoint-state-sha-mismatch'); + console.log('Game context after:', clone(this.__gameContext)); + + this.__invokeEventCallback(new Init()); + + for (const h of frame.histories) { + await this.__handleEvent(h.event, h.timestamp, h.stateSha); + } + this.__invokeEventCallback(new EndOfHistory()) + } finally { + console.groupEnd(); + } + } + } + + async __startSubscribe(): Promise { + await this.__connection.connect(new SubscribeEventParams({ settleVersion: this.__gameContext.settleVersion })); + } + + async __handleConnectionState(state: ConnectionState) { + if (state === 'disconnected') { + if (this.__onConnectionState !== undefined) { + this.__onConnectionState('disconnected') + } + console.log('Disconnected, try reset state and context'); + await this.__startSubscribe(); + } else if (state === 'connected') { + if (this.__onConnectionState !== undefined) { + this.__onConnectionState('connected') + } + } else if (state === 'closed') { + if (this.__onConnectionState !== undefined) { + this.__onConnectionState('closed') + } + } else if (state === 'reconnected') { + if (this.__onConnectionState !== undefined) { + this.__onConnectionState('reconnected') + } + } + } + + get gameId(): number { + return this.__gameId; + } +} diff --git a/js/sdk-core/src/broadcast-frames.ts b/js/sdk-core/src/broadcast-frames.ts new file mode 100644 index 00000000..cee8579a --- /dev/null +++ b/js/sdk-core/src/broadcast-frames.ts @@ -0,0 +1,120 @@ +import { TxState } from './tx-state'; +import { PlayerJoin, ServerJoin } from './accounts'; +import { array, enums, field, option, struct, variant } from '@race-foundation/borsh'; +import { EventHistory, GameEvent } from './events'; +import { CheckpointOffChain } from './checkpoint'; + +export type BroadcastFrameKind = + | 'Invalid' + | 'Event' + | 'Message' + | 'TxState' + | 'Sync' + | 'EventHistories' + +export class Message { + @field('string') + sender!: string; + @field('string') + content!: string; + constructor(fields: any) { + Object.assign(this, fields); + } +} + +export abstract class BroadcastFrame { + kind(): BroadcastFrameKind { + return 'Invalid' + } +} + +@variant(0) +export class BroadcastFrameEvent extends BroadcastFrame { + @field('string') + target!: string; + @field(enums(GameEvent)) + event!: GameEvent; + @field('u64') + timestamp!: bigint; + @field('string') + stateSha!: string; + constructor(fields: any) { + super(); + Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameEvent.prototype); + } + kind(): BroadcastFrameKind { + return 'Event' + } +} + +@variant(1) +export class BroadcastFrameMessage extends BroadcastFrame { + @field('string') + target!: string; + @field(struct(Message)) + message!: Message; + constructor(fields: any) { + super(); + Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameMessage.prototype); + } + kind(): BroadcastFrameKind { + return 'Message' + } +} + +@variant(2) +export class BroadcastFrameTxState extends BroadcastFrame { + @field(enums(TxState)) + txState!: TxState; + constructor(fields: any) { + super(); + Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameTxState.prototype); + } + kind(): BroadcastFrameKind { + return 'TxState' + } +} + +@variant(3) +export class BroadcastFrameSync extends BroadcastFrame { + @field(array(struct(PlayerJoin))) + newPlayers!: PlayerJoin[]; + @field(array(struct(ServerJoin))) + newServers!: ServerJoin[]; + @field('string') + transactor_addr!: string; + @field('u64') + accessVersion!: bigint; + constructor(fields: any) { + super(); + Object.assign(this, fields) + Object.setPrototypeOf(this, BroadcastFrameSync.prototype); + } + kind(): BroadcastFrameKind { + return 'Sync' + } +} + +@variant(4) +export class BroadcastFrameEventHistories extends BroadcastFrame { + @field('string') + gameAddr!: string; + @field(option(struct(CheckpointOffChain))) + checkpointOffChain: CheckpointOffChain | undefined; + @field(array(struct(EventHistory))) + histories!: EventHistory[]; + @field('string') + stateSha!: string; + + constructor(fields: any) { + super(); + Object.assign(this, fields); + Object.setPrototypeOf(this, BroadcastFrameEventHistories.prototype); + } + kind(): BroadcastFrameKind { + return 'EventHistories' + } +} diff --git a/js/sdk-core/src/checkpoint.ts b/js/sdk-core/src/checkpoint.ts new file mode 100644 index 00000000..2ef6aca9 --- /dev/null +++ b/js/sdk-core/src/checkpoint.ts @@ -0,0 +1,160 @@ +import { deserialize, field, map, struct } from '@race-foundation/borsh'; +import { sha256 } from './encryptor'; + +export class CheckpointOnChain { + @field('u8-array') + root!: Uint8Array + + @field('usize') + size!: number + + @field('u64') + accessVersion!: bigint + + constructor(fields: any) { + Object.assign(this, fields) + } + + static fromRaw(raw: Uint8Array): CheckpointOnChain { + return deserialize(CheckpointOnChain, raw); + } +} + +export class VersionedData { + @field('usize') + id!: number; + + @field('u64') + version!: bigint; + + @field('u8-array') + data!: Uint8Array; + + @field('u8-array') + sha!: Uint8Array; + + constructor(fields: any) { + Object.assign(this, fields); + } +} + +export class CheckpointOffChain { + @field(map('usize', struct(VersionedData))) + data!: Map + + @field(map('usize', 'u8-array')) + proofs!: Map + + constructor(fields: any) { + Object.assign(this, fields); + } + + static deserialize(raw: Uint8Array): CheckpointOffChain { + return deserialize(CheckpointOffChain, raw) + } +} + +/// Represent the on-chain checkpoint. +export class Checkpoint { + @field('u8-array') + root!: Uint8Array; + + @field('u64') + accessVersion!: bigint; + + @field(map('usize', struct(VersionedData))) + data!: Map; + + @field(map('usize', 'u8-array')) + proofs!: Map; + + constructor(fields: any) { + Object.assign(this, fields) + } + + static default(): Checkpoint { + return new Checkpoint({ accessVersion: 0n, data: new Map() }) + } + + static fromParts(offchainPart: CheckpointOffChain, onchainPart: CheckpointOnChain): Checkpoint { + console.debug('Build checkpoint, offchain:', offchainPart, 'onchain:', onchainPart); + let checkpoint = Checkpoint.default(); + checkpoint.proofs = offchainPart.proofs; + checkpoint.data = offchainPart.data; + checkpoint.accessVersion = onchainPart.accessVersion; + checkpoint.root = onchainPart.root; + return checkpoint; + } + + static fromRaw(raw: Uint8Array): Checkpoint { + if (raw.length === 0) { + return Checkpoint.default(); + } + return deserialize(Checkpoint, raw); + } + + static fromData(id: number, version: bigint, data: Uint8Array): Checkpoint { + return new Checkpoint({ + data: new Map([[id, new VersionedData({ + version, data + })]]) + }); + } + + clone(): Checkpoint { + return new Checkpoint({ + accessVersion: this.accessVersion, + data: new Map(this.data.entries()) + }) + } + + getData(id: number): Uint8Array | undefined { + return this.data.get(id)?.data; + } + + getSha(id: number): Uint8Array | undefined { + return this.data.get(id)?.sha; + } + + setVersion(id: number, version: bigint) { + const data = this.data.get(id); + if (data !== undefined) { + data.version = version; + } + } + + async setData(id: number, data: Uint8Array) { + const sha = await sha256(data); + const old = this.data.get(id); + if (old !== undefined) { + old.data = data; + old.version += 1n; + old.sha = sha; + } else { + this.data.set(id, new VersionedData({ + id, sha, data, version: 1n + })) + } + this.updateRootAndProofs(); + } + + updateRootAndProofs() { + + } + + + setAccessVersion(accessVersion: bigint) { + this.accessVersion = accessVersion; + } + + __setVersion(id: number, version: bigint) { + let vd = this.data.get(id); + if (vd !== undefined) { + vd.version = version; + } + } + + getVersion(id: number): bigint { + return this.data.get(id)?.version || 0n + } +} diff --git a/js/sdk-core/src/client.ts b/js/sdk-core/src/client.ts index 7261ab7f..8d9bf28d 100644 --- a/js/sdk-core/src/client.ts +++ b/js/sdk-core/src/client.ts @@ -1,7 +1,6 @@ -import { AttachGameParams, IConnection, SubmitEventParams } from './connection'; +import { AttachGameParams, AttachResponse, IConnection, SubmitEventParams } from './connection'; import { IEncryptor } from './encryptor'; import { SecretState } from './secret-state'; -import { makeCustomEvent } from './events'; import { GameContext } from './game-context'; import { Id } from './types'; @@ -26,57 +25,46 @@ type OpIdent = }; export class Client { - #encryptor: IEncryptor; - #connection: IConnection; - #gameAddr: string; - #addr: string; - #opHist: OpIdent[]; - #secretState: SecretState; + __encryptor: IEncryptor; + __connection: IConnection; + __addr: string; + __opHist: OpIdent[]; + __secretState: SecretState; - constructor(addr: string, gameAddr: string, encryptor: IEncryptor, connection: IConnection) { - this.#addr = addr; - this.#gameAddr = gameAddr; - this.#encryptor = encryptor; - this.#connection = connection; - this.#opHist = new Array(); - this.#secretState = new SecretState(encryptor); + constructor(addr: string, encryptor: IEncryptor, connection: IConnection) { + this.__addr = addr; + this.__encryptor = encryptor; + this.__connection = connection; + this.__opHist = new Array(); + this.__secretState = new SecretState(encryptor); } - async attachGame(): Promise { - const key = await this.#encryptor.exportPublicKey(undefined); - await this.#connection.attachGame( + async attachGame(): Promise { + const key = await this.__encryptor.exportPublicKey(undefined); + return await this.__connection.attachGame( new AttachGameParams({ - signer: this.#addr, + signer: this.__addr, key, }) ); } async submitEvent(event: any): Promise { - await this.#connection.submitEvent( + await this.__connection.submitEvent( new SubmitEventParams({ event, }) ); } - async submitCustomEvent(customEvent: any): Promise { - const event = makeCustomEvent(this.#gameAddr, customEvent); - await this.#connection.submitEvent( - new SubmitEventParams({ - event, - }) - ); - } - - async handleDecision(ctx: GameContext): Promise { + async handleDecision(_ctx: GameContext): Promise { return []; } loadRandomStates(ctx: GameContext) { for (let randomState of ctx.randomStates) { - if (!this.#secretState.isRandomLoaded(randomState.id)) { - this.#secretState.genRandomStates(randomState.id, randomState.size); + if (!this.__secretState.isRandomLoaded(randomState.id)) { + this.__secretState.genRandomStates(randomState.id, randomState.size); } } } @@ -88,21 +76,21 @@ export class Client { } flushSecretStates() { - this.#secretState.clear(); - this.#opHist.splice(0); + this.__secretState.clear(); + this.__opHist.splice(0); } async decrypt(ctx: GameContext, randomId: Id): Promise> { let randomState = ctx.getRandomState(randomId); let options = randomState.options; - let revealed = await this.#encryptor.decryptWithSecrets( + let revealed = await this.__encryptor.decryptWithSecrets( randomState.listRevealedCiphertexts(), randomState.listRevealedSecrets(), options ); - let assigned = await this.#encryptor.decryptWithSecrets( - randomState.listAssignedCiphertexts(this.#addr), - randomState.listSharedSecrets(this.#addr), + let assigned = await this.__encryptor.decryptWithSecrets( + randomState.listAssignedCiphertexts(this.__addr), + randomState.listSharedSecrets(this.__addr), options ); diff --git a/js/sdk-core/src/connection.ts b/js/sdk-core/src/connection.ts index 30a5c57d..eab3b0bd 100644 --- a/js/sdk-core/src/connection.ts +++ b/js/sdk-core/src/connection.ts @@ -1,12 +1,22 @@ import { IEncryptor, PublicKeyRaws } from './encryptor'; -import { TxState } from './tx-state'; import { GameEvent } from './events'; -import { deserialize, array, enums, field, option, serialize, struct, variant } from '@race-foundation/borsh'; +import { deserialize, enums, field, serialize, struct } from '@race-foundation/borsh'; import { arrayBufferToBase64, base64ToUint8Array } from './utils'; +import { BroadcastFrame } from './broadcast-frames'; +import { CheckpointOffChain } from './checkpoint'; export type ConnectionState = 'disconnected' | 'connected' | 'reconnected' | 'closed'; -type Method = 'attach_game' | 'submit_event' | 'exit_game' | 'subscribe_event' | 'submit_message' | 'ping'; +export type AttachResponse = 'success' | 'game-not-loaded'; + +type Method = 'attach_game' + | 'submit_event' + | 'exit_game' + | 'subscribe_event' + | 'submit_message' + | 'get_state' + | 'ping' + | 'checkpoint'; interface IAttachGameParams { signer: string; @@ -25,11 +35,14 @@ interface ISubmitMessageParams { content: string; } -interface ICheckTxStateParams { - newPlayers: string[]; - accessVersion: bigint; +interface IGetCheckpointParams { + settleVersion: bigint; } +export type ConnectionSubscriptionItem = BroadcastFrame | ConnectionState | undefined; + +export type ConnectionSubscription = AsyncGenerator; + export class AttachGameParams { @field('string') signer: string; @@ -70,56 +83,20 @@ export class SubmitMessageParams { } } -export class Message { - @field('string') - sender!: string; - @field('string') - content!: string; - constructor(fields: any) { - Object.assign(this, fields); - } -} - -export abstract class BroadcastFrame { } - -@variant(0) -export class BroadcastFrameEvent extends BroadcastFrame { - @field('string') - gameAddr!: string; - @field(enums(GameEvent)) - event!: GameEvent; +export class GetCheckpointParams { @field('u64') - timestamp!: bigint; - constructor(fields: any) { - super(); - Object.assign(this, fields); + settleVersion: bigint; + constructor(fields: IGetCheckpointParams) { + this.settleVersion = fields.settleVersion; } } -@variant(1) -export class BroadcastFrameMessage extends BroadcastFrame { - @field('string') - gameAddr!: string; - @field(struct(Message)) - message!: Message; - constructor(fields: any) { - super(); - Object.assign(this, fields); - } -} +export interface IConnection { + attachGame(params: AttachGameParams): Promise; -@variant(2) -export class BroadcastFrameTxState extends BroadcastFrame { - @field(enums(TxState)) - txState!: TxState; - constructor(fields: any) { - super(); - Object.assign(this, fields); - } -} + getState(): Promise; -export interface IConnection { - attachGame(params: AttachGameParams): Promise; + getCheckpoint(params: GetCheckpointParams): Promise; submitEvent(params: SubmitEventParams): Promise; @@ -129,11 +106,17 @@ export interface IConnection { connect(params: SubscribeEventParams): Promise; - subscribeEvents(): AsyncGenerator; + disconnect(): void; + + subscribeEvents(): ConnectionSubscription; } +type StreamMessageType = BroadcastFrame | ConnectionState | undefined; + export class Connection implements IConnection { - gameAddr: string; + // The target to connect, in normal game the target is the address + // of game. In a sub game, the target is constructed as ADDR:ID. + target: string; playerAddr: string; endpoint: string; encryptor: IEncryptor; @@ -142,9 +125,9 @@ export class Connection implements IConnection { closed: boolean; // For async message stream - streamResolve?: ((value: BroadcastFrame | ConnectionState | undefined) => void); - streamMessageQueue: BroadcastFrame[]; - streamMessagePromise?: Promise; + streamResolve?: ((value: StreamMessageType) => void); + streamMessageQueue: StreamMessageType[]; + streamMessagePromise?: Promise; // For keep alive lastPong: number; @@ -152,8 +135,8 @@ export class Connection implements IConnection { isFirstOpen: boolean; - constructor(gameAddr: string, playerAddr: string, endpoint: string, encryptor: IEncryptor) { - this.gameAddr = gameAddr; + constructor(target: string, playerAddr: string, endpoint: string, encryptor: IEncryptor) { + this.target = target; this.playerAddr = playerAddr; this.endpoint = endpoint; this.encryptor = encryptor; @@ -197,7 +180,7 @@ export class Connection implements IConnection { } async connect(params: SubscribeEventParams) { - console.log('Establishing server connection, settle version:', params.settleVersion); + console.log(`Establishing server connection, target: ${this.target}, settle version: ${params.settleVersion}`) this.socket = new WebSocket(this.endpoint); this.clearCheckTimer(); @@ -235,7 +218,7 @@ export class Connection implements IConnection { return; } if (this.socket !== undefined && this.socket.readyState === this.socket.OPEN) { - this.socket.send(this.makeReqNoSig(this.gameAddr, 'ping', {})); + this.socket.send(this.makeReqNoSig(this.target, 'ping', {})); } }, 3000); @@ -260,18 +243,36 @@ export class Connection implements IConnection { }; // Call JSONRPC subscribe_event - const req = this.makeReqNoSig(this.gameAddr, 'subscribe_event', params); + const req = this.makeReqNoSig(this.target, 'subscribe_event', params); await this.requestWs(req); } - async attachGame(params: AttachGameParams): Promise { - const req = this.makeReqNoSig(this.gameAddr, 'attach_game', params); - await this.requestXhr(req); + async attachGame(params: AttachGameParams): Promise { + const req = this.makeReqNoSig(this.target, 'attach_game', params); + const resp: any = await this.requestXhr(req); + if (resp.error !== undefined) { + return 'game-not-loaded'; + } else { + return 'success'; + } + } + + async getState(): Promise { + const req = this.makeReqNoSig(this.target, 'get_state', {}); + const resp: { result: string } = await this.requestXhr(req); + return Uint8Array.from(JSON.parse(resp.result)); + } + + async getCheckpoint(params: GetCheckpointParams): Promise { + const req = this.makeReqNoSig(this.target, 'checkpoint', params) + const resp: { result: number[] | null } = await this.requestXhr(req); + if (!resp.result) return undefined; + return CheckpointOffChain.deserialize(Uint8Array.from(resp.result)); } async submitEvent(params: SubmitEventParams): Promise { try { - const req = await this.makeReq(this.gameAddr, 'submit_event', params); + const req = await this.makeReq(this.target, 'submit_event', params); await this.requestXhr(req); return undefined; } catch (_: any) { @@ -281,7 +282,7 @@ export class Connection implements IConnection { async submitMessage(params: SubmitMessageParams): Promise { try { - const req = await this.makeReq(this.gameAddr, 'submit_message', params); + const req = await this.makeReq(this.target, 'submit_message', params); await this.requestXhr(req); return undefined; } catch (_: any) { @@ -289,55 +290,66 @@ export class Connection implements IConnection { } } + disconnect() { + if (this.socket !== undefined) { + this.closed = true; + this.socket.close(); + this.socket = undefined; + } + } + async exitGame(params: ExitGameParams): Promise { - const req = await this.makeReq(this.gameAddr, 'exit_game', {}); + const req = await this.makeReq(this.target, 'exit_game', {}); await this.requestXhr(req); - if (!params.keepConnection) { - if (this.socket !== undefined) { - this.closed = true; - this.socket.close(); - this.socket = undefined; - } - } + if (!params.keepConnection) this.disconnect(); } async *subscribeEvents(): AsyncGenerator { await this.waitSocketReady(); this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); while (true) { - if (this.streamMessageQueue.length > 0) { - yield this.streamMessageQueue.shift()!; + while (this.streamMessageQueue.length > 0) { + yield this.streamMessageQueue.shift(); + } + if (this.streamResolve === undefined) { + this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); + yield this.streamMessagePromise; } else { yield this.streamMessagePromise; - this.streamMessagePromise = new Promise(r => (this.streamResolve = r)); } } } parseEventMessage(raw: string): BroadcastFrame | ConnectionState | undefined { - let resp = JSON.parse(raw); - if (resp.result === 'pong') { - this.lastPong = new Date().getTime(); - return undefined; - } else if (resp.method === 's_event') { - if (resp.params.error === undefined) { - let result: string = resp.params.result; - let data = base64ToUint8Array(result); - let frame = deserialize(BroadcastFrame, data); - return frame; + try { + let resp = JSON.parse(raw); + if (resp.result === 'pong') { + this.lastPong = new Date().getTime(); + return undefined; + } else if (resp.method === 's_event') { + if (resp.params.error === undefined) { + let result: string = resp.params.result; + let data = base64ToUint8Array(result); + let frame = deserialize(BroadcastFrame, data); + return frame; + } else { + return 'disconnected' + } } else { - return 'disconnected' + return undefined; } - } else { - return undefined; + } catch (e) { + console.error(`Parse event message error: ${raw}`) + throw e } } - static initialize(gameAddr: string, playerAddr: string, endpoint: string, encryptor: IEncryptor): Connection { - return new Connection(gameAddr, playerAddr, endpoint, encryptor); + static initialize(target: string, playerAddr: string, endpoint: string, encryptor: IEncryptor): Connection { + return new Connection(target, playerAddr, endpoint, encryptor); } - async makeReq

(gameAddr: string, method: Method, params: P): Promise { + async makeReq

(target: string, method: Method, params: P): Promise { + console.log(`Connection request, target: ${target}, method: ${method}, params:`, params) const paramsBytes = serialize(params); const sig = await this.encryptor.sign(paramsBytes, this.playerAddr); const sigBytes = serialize(sig); @@ -345,17 +357,20 @@ export class Connection implements IConnection { jsonrpc: '2.0', method, id: crypto.randomUUID(), - params: [gameAddr, arrayBufferToBase64(paramsBytes), arrayBufferToBase64(sigBytes)], + params: [target, arrayBufferToBase64(paramsBytes), arrayBufferToBase64(sigBytes)], }); } - makeReqNoSig

(gameAddr: string, method: Method, params: P): string { + makeReqNoSig

(target: string, method: Method, params: P): string { + if (method !== 'ping') { + console.log(`Connection request[NoSig], target: ${target}, method: ${method}, params:`, params) + } const paramsBytes = serialize(params); return JSON.stringify({ jsonrpc: '2.0', method, id: crypto.randomUUID(), - params: [gameAddr, arrayBufferToBase64(paramsBytes)], + params: [target, arrayBufferToBase64(paramsBytes)], }); } @@ -381,7 +396,9 @@ export class Connection implements IConnection { }, }); if (resp.ok) { - return resp.json(); + const ret = await resp.json(); + console.debug('Response:', ret); + return ret; } else { throw Error('Transactor request failed:' + resp.json()); } diff --git a/js/sdk-core/src/effect.ts b/js/sdk-core/src/effect.ts index 17b39648..de0e13dd 100644 --- a/js/sdk-core/src/effect.ts +++ b/js/sdk-core/src/effect.ts @@ -3,6 +3,7 @@ import { HandleError } from './error'; import { GameContext } from './game-context'; import { enums, field, map, option, struct, variant, array } from '@race-foundation/borsh'; import { Fields, Id } from './types'; +import { GamePlayer, InitAccount } from './init-account'; export abstract class SettleOp {} @@ -34,12 +35,12 @@ export class SettleEject extends SettleOp { } export class Settle { - @field('string') - addr: string; + @field('u64') + id: bigint; @field(enums(SettleOp)) op: SettleOp; - constructor(fields: { addr: string; op: SettleOp }) { - this.addr = fields.addr; + constructor(fields: { id: bigint; op: SettleOp }) { + this.id = fields.id; this.op = fields.op; } sortKey(): number { @@ -77,8 +78,8 @@ export class Ask { export class Assign { @field('usize') randomId!: number; - @field('string') - playerAddr!: string; + @field('u64') + playerId!: bigint; @field(array('usize')) indexes!: number[]; constructor(fields: Fields) { @@ -105,8 +106,8 @@ export class Release { } export class ActionTimeout { - @field('string') - playerAddr!: string; + @field('u64') + playerId!: bigint; @field('u64') timeout!: bigint; constructor(fields: Fields) { @@ -114,6 +115,31 @@ export class ActionTimeout { } } +export class SubGame { + @field('usize') + gameId!: number; + @field('string') + bundleAddr!: string; + @field(struct(InitAccount)) + initAccount!: InitAccount; + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +export class EmitBridgeEvent { + @field('usize') + dest!: number; + @field('u8-array') + raw!: Uint8Array; + @field(array(struct(GamePlayer))) + joinPlayers!: GamePlayer[]; + + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + export class Effect { @field(option(struct(ActionTimeout))) actionTimeout: ActionTimeout | undefined; @@ -140,10 +166,7 @@ export class Effect { currDecisionId!: number; @field('u16') - playersCount!: number; - - @field('u16') - serversCount!: number; + nodesCount!: number; @field(array(struct(Ask))) asks!: Ask[]; @@ -169,9 +192,6 @@ export class Effect { @field('bool') isCheckpoint!: boolean; - @field(option('u8-array')) - checkpoint!: Uint8Array | undefined; - @field(array(struct(Settle))) settles!: Settle[]; @@ -187,11 +207,23 @@ export class Effect { @field(array(struct(Transfer))) transfers!: Transfer[]; + @field(array(struct(SubGame))) + launchSubGames!: SubGame[]; + + @field(array(struct(EmitBridgeEvent))) + bridgeEvents!: EmitBridgeEvent[]; + + @field(array(struct(GamePlayer))) + validPlayers!: GamePlayer[]; + + @field('bool') + isInit!: boolean; + constructor(fields: Fields) { Object.assign(this, fields); } - static fromContext(context: GameContext) { + static fromContext(context: GameContext, isInit: boolean) { const revealed = new Map>(); for (const st of context.randomStates) { revealed.set(st.id, st.revealed); @@ -205,23 +237,24 @@ export class Effect { const startGame = false; const stopGame = false; const cancelDispatch = false; - const timestamp = context.timestamp; + const timestamp = isInit ? 0n : context.timestamp; const currRandomId = context.randomStates.length + 1; const currDecisionId = context.decisionStates.length + 1; - const playersCount = context.players.length; - const serversCount = context.servers.length; + const nodesCount = context.nodes.length; const asks: Ask[] = []; const assigns: Assign[] = []; const releases: Release[] = []; const reveals: Reveal[] = []; const initRandomStates: RandomSpec[] = []; const isCheckpoint = false; - const checkpoint = undefined; const settles: Settle[] = []; const handlerState = context.handlerState; const error = undefined; const allowExit = context.allowExit; const transfers: Transfer[] = []; + const launchSubGames: SubGame[] = []; + const bridgeEvents: EmitBridgeEvent[] = []; + const validPlayers = context.players; return new Effect({ actionTimeout, waitTimeout, @@ -231,8 +264,7 @@ export class Effect { timestamp, currRandomId, currDecisionId, - playersCount, - serversCount, + nodesCount, asks, assigns, releases, @@ -241,12 +273,15 @@ export class Effect { revealed, answered, isCheckpoint, - checkpoint, settles, handlerState, error, allowExit, transfers, + launchSubGames, + bridgeEvents, + validPlayers, + isInit }); } } diff --git a/js/sdk-core/src/encryptor.ts b/js/sdk-core/src/encryptor.ts index cc9649d7..b6f62453 100644 --- a/js/sdk-core/src/encryptor.ts +++ b/js/sdk-core/src/encryptor.ts @@ -9,7 +9,7 @@ const ENCRYPTOR_VERSION = '1.0'; let subtle: SubtleCrypto; if (typeof window === 'undefined') { - const crypto = require('node:crypto') + const crypto = require('crypto') subtle = crypto.subtle } else { subtle = window.crypto.subtle @@ -457,3 +457,15 @@ export class Encryptor implements IEncryptor { return res; } } + +export async function sha256(data: Uint8Array): Promise { + let hashBuffer = await subtle.digest('SHA-256', data) + return new Uint8Array(hashBuffer); +} + + +export async function sha256String(data: Uint8Array): Promise { + return Array.from(await sha256(data)) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); +} diff --git a/js/sdk-core/src/error.ts b/js/sdk-core/src/error.ts index c6dd8ec5..d1e986c8 100644 --- a/js/sdk-core/src/error.ts +++ b/js/sdk-core/src/error.ts @@ -21,10 +21,10 @@ export class NoEnoughPlayers extends HandleError { } @variant(2) -export class PlayerNotInGame extends HandleError { +export class InvalidPlayer extends HandleError { constructor(_: any) { super(); - this.message = 'Player not in game'; + this.message = 'Invalid player'; } } @@ -53,6 +53,14 @@ export class MalformedGameAccountData extends HandleError { } @variant(6) +export class MalformedCheckpointData extends HandleError { + constructor(_: any) { + super(); + this.message = 'Malformed checkpoint data'; + } +} + +@variant(7) export class MalformedCustomEvent extends HandleError { constructor(_: any) { super(); @@ -60,7 +68,15 @@ export class MalformedCustomEvent extends HandleError { } } -@variant(7) +@variant(8) +export class MalformedBridgeEvent extends HandleError { + constructor(_: any) { + super(); + this.message = 'Malformed bridge event'; + } +} + +@variant(9) export class SerializationError extends HandleError { constructor(_: any) { super(); @@ -68,7 +84,7 @@ export class SerializationError extends HandleError { } } -@variant(8) +@variant(10) export class NoEnoughServers extends HandleError { constructor(_: any) { super(); @@ -76,7 +92,7 @@ export class NoEnoughServers extends HandleError { } } -@variant(9) +@variant(11) export class InternalError extends HandleError { @field('string') message: string; @@ -114,4 +130,8 @@ export class SdkError extends Error { static tokenNotFound(addr: string) { return new SdkError(`Token ${addr} not found`); } + + static invalidSubId(gameId: number) { + return new SdkError(`Invalid gameId: ${gameId}`); + } } diff --git a/js/sdk-core/src/events.ts b/js/sdk-core/src/events.ts index 1f361268..0aef51eb 100644 --- a/js/sdk-core/src/events.ts +++ b/js/sdk-core/src/events.ts @@ -1,6 +1,6 @@ import { field, array, enums, option, variant, struct } from '@race-foundation/borsh'; -import { PlayerJoin, ServerJoin } from './accounts'; import { Fields, Id } from './types'; +import { GamePlayer } from './init-account'; type EventFields = Omit, 'kind'>; @@ -13,7 +13,7 @@ export type EventKind = | 'Mask' | 'Lock' | 'RandomnessReady' - | 'Sync' + | 'Join' | 'ServerLeave' | 'Leave' | 'GameStart' @@ -23,12 +23,21 @@ export type EventKind = | 'ActionTimeout' | 'AnswerDecision' | 'SecretsReady' - | 'Shutdown'; + | 'Shutdown' + | 'Bridge' + // Client-only events + | 'Init' + | 'Checkpoint' + | 'EndOfHistory'; export interface ICustomEvent { serialize(): Uint8Array; } +export interface IBridgeEvent { + serialize(): Uint8Array; +} + interface IEventKind { kind(): EventKind; } @@ -50,6 +59,7 @@ export class Random extends SecretShare { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Random.prototype) } } @@ -64,6 +74,7 @@ export class Answer extends SecretShare { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Answer.prototype) } } @@ -73,22 +84,36 @@ export abstract class GameEvent implements IEventKind { } } +export class EventHistory { + @field(enums(GameEvent)) + event!: GameEvent; + @field('u64') + timestamp!: bigint; + @field('string') + stateSha!: string; + constructor(fields: EventFields) { + Object.assign(this, fields); + Object.setPrototypeOf(this, EventHistory.prototype); + } +} + @variant(0) export class Custom extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field('u8-array') raw!: Uint8Array; constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Custom.prototype) } kind(): EventKind { return 'Custom'; } } -export function makeCustomEvent(sender: string, customEvent: ICustomEvent): Custom { +export function makeCustomEvent(sender: bigint, customEvent: ICustomEvent): Custom { return new Custom({ sender, raw: customEvent.serialize(), @@ -99,6 +124,7 @@ export function makeCustomEvent(sender: string, customEvent: ICustomEvent): Cust export class Ready extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, Ready.prototype) } kind(): EventKind { return 'Ready'; @@ -107,13 +133,14 @@ export class Ready extends GameEvent implements IEventKind { @variant(2) export class ShareSecrets extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field(array(enums(SecretShare))) shares!: SecretShare[]; constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ShareSecrets.prototype) } kind(): EventKind { return 'ShareSecrets'; @@ -122,10 +149,11 @@ export class ShareSecrets extends GameEvent implements IEventKind { @variant(3) export class OperationTimeout extends GameEvent implements IEventKind { - @field(array('string')) - addrs!: string[]; + @field(array('u64')) + ids!: bigint[]; constructor(fields: EventFields) { super(); + Object.setPrototypeOf(this, OperationTimeout.prototype) Object.assign(this, fields); } kind(): EventKind { @@ -135,8 +163,8 @@ export class OperationTimeout extends GameEvent implements IEventKind { @variant(4) export class Mask extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field('usize') randomId!: Id; @field(array('u8-array')) @@ -144,6 +172,7 @@ export class Mask extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Mask.prototype) } kind(): EventKind { return 'Mask'; @@ -162,8 +191,8 @@ export class CiphertextAndDigest { @variant(5) export class Lock extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field('usize') randomId!: Id; @field(array(struct(CiphertextAndDigest))) @@ -171,6 +200,7 @@ export class Lock extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Lock.prototype) } kind(): EventKind { return 'Lock'; @@ -184,6 +214,7 @@ export class RandomnessReady extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, RandomnessReady.prototype) } kind(): EventKind { return 'RandomnessReady'; @@ -191,33 +222,27 @@ export class RandomnessReady extends GameEvent implements IEventKind { } @variant(7) -export class Sync extends GameEvent implements IEventKind { - @field(array(struct(PlayerJoin))) - newPlayers!: PlayerJoin[]; - @field(array(struct(ServerJoin))) - newServers!: ServerJoin[]; - @field('string') - transactorAddr!: string; - @field('u64') - accessVersion!: bigint; - constructor(fields: EventFields) { +export class Join extends GameEvent implements IEventKind { + @field(array(struct(GamePlayer))) + players!: GamePlayer[]; + constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Join.prototype) } kind(): EventKind { - return 'Sync'; + return 'Join'; } } @variant(8) export class ServerLeave extends GameEvent implements IEventKind { - @field('string') - serverAddr!: string; - @field('string') - transactorAddr!: string; + @field('u64') + serverId!: bigint; constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ServerLeave.prototype) } kind(): EventKind { return 'ServerLeave'; @@ -226,11 +251,12 @@ export class ServerLeave extends GameEvent implements IEventKind { @variant(9) export class Leave extends GameEvent implements IEventKind { - @field('string') - playerAddr!: string; + @field('u64') + playerId!: bigint; constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, Leave.prototype) } kind(): EventKind { return 'Leave'; @@ -239,11 +265,9 @@ export class Leave extends GameEvent implements IEventKind { @variant(10) export class GameStart extends GameEvent implements IEventKind { - @field('u64') - accessVersion!: bigint; - constructor(fields: EventFields) { + constructor(_: any = {}) { super(); - Object.assign(this, fields); + Object.setPrototypeOf(this, GameStart.prototype) } kind(): EventKind { return 'GameStart'; @@ -254,6 +278,7 @@ export class GameStart extends GameEvent implements IEventKind { export class WaitingTimeout extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, WaitingTimeout.prototype) } kind(): EventKind { return 'WaitingTimeout'; @@ -262,8 +287,8 @@ export class WaitingTimeout extends GameEvent implements IEventKind { @variant(12) export class DrawRandomItems extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field('usize') randomId!: Id; @field(array('usize')) @@ -271,6 +296,7 @@ export class DrawRandomItems extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, DrawRandomItems.prototype) } kind(): EventKind { return 'DrawRandomItems'; @@ -281,6 +307,7 @@ export class DrawRandomItems extends GameEvent implements IEventKind { export class DrawTimeout extends GameEvent implements IEventKind { constructor(_: {}) { super(); + Object.setPrototypeOf(this, DrawTimeout.prototype) } kind(): EventKind { return 'DrawTimeout'; @@ -289,11 +316,12 @@ export class DrawTimeout extends GameEvent implements IEventKind { @variant(14) export class ActionTimeout extends GameEvent implements IEventKind { - @field('string') - playerAddr!: string; + @field('u64') + playerId!: bigint; constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, ActionTimeout.prototype) } kind(): EventKind { return 'ActionTimeout'; @@ -302,8 +330,8 @@ export class ActionTimeout extends GameEvent implements IEventKind { @variant(15) export class AnswerDecision extends GameEvent implements IEventKind { - @field('string') - sender!: string; + @field('u64') + sender!: bigint; @field('usize') decisionId!: Id; @field('u8-array') @@ -313,6 +341,7 @@ export class AnswerDecision extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, AnswerDecision.prototype) } kind(): EventKind { return 'AnswerDecision'; @@ -327,6 +356,7 @@ export class SecretsReady extends GameEvent implements IEventKind { constructor(fields: EventFields) { super(); Object.assign(this, fields); + Object.setPrototypeOf(this, SecretsReady.prototype) } kind(): EventKind { return 'SecretsReady'; @@ -337,8 +367,58 @@ export class SecretsReady extends GameEvent implements IEventKind { export class Shutdown extends GameEvent implements IEventKind { constructor(_: any = {}) { super(); + Object.setPrototypeOf(this, Shutdown.prototype) } kind(): EventKind { return 'Shutdown'; } } + +@variant(18) +export class Bridge extends GameEvent implements IEventKind { + @field('usize') + dest!: number; + @field('u8-array') + raw!: Uint8Array; + @field(array(struct(GamePlayer))) + joinPlayers!: GamePlayer[]; + + constructor(fields: EventFields) { + super(); + Object.assign(this, fields); + Object.setPrototypeOf(this, Bridge.prototype) + } + + kind(): EventKind { + return 'Bridge'; + } +} + +// Client-only events, they can't be serialized and deserialized. + +export class Init extends GameEvent implements IEventKind { + constructor() { + super(); + } + kind(): EventKind { + return 'Init' + } +} + +export class CheckpointReady extends GameEvent implements IEventKind { + constructor() { + super(); + } + kind(): EventKind { + return 'Checkpoint' + } +} + +export class EndOfHistory extends GameEvent implements IEventKind { + constructor() { + super(); + } + kind(): EventKind { + return 'EndOfHistory' + } +} diff --git a/js/sdk-core/src/game-context-snapshot.ts b/js/sdk-core/src/game-context-snapshot.ts index bac62e87..ee2dce8b 100644 --- a/js/sdk-core/src/game-context-snapshot.ts +++ b/js/sdk-core/src/game-context-snapshot.ts @@ -1,29 +1,11 @@ -import { PlayerProfile } from './accounts'; -import { GameContext, GameStatus, IPlayer, IServer, NodeStatus } from './game-context'; +import { GameContext, GameStatus, INode, NodeStatus } from './game-context'; -export class PlayerShapshot { +export class NodeSnapshot { readonly addr: string; - readonly position: number; - readonly balance: bigint; readonly status: NodeStatus; - profile?: PlayerProfile; - constructor(o: IPlayer) { + constructor(o: INode) { this.addr = o.addr; - this.balance = o.balance; - this.position = o.position; - this.status = o.status; - } -} - -export class ServerShapshot { - readonly addr: string; - readonly endpoint: string; - readonly status: NodeStatus; - - constructor(o: IServer) { - this.addr = o.addr; - this.endpoint = o.endpoint; this.status = o.status; } } @@ -34,8 +16,7 @@ export class GameContextSnapshot { readonly settleVersion: bigint; readonly status: GameStatus; readonly allowExit: boolean; - readonly players: PlayerShapshot[]; - readonly servers: ServerShapshot[]; + readonly nodes: NodeSnapshot[]; constructor(context: GameContext) { this.gameAddr = context.gameAddr; @@ -43,7 +24,6 @@ export class GameContextSnapshot { this.settleVersion = context.settleVersion; this.status = context.status; this.allowExit = context.allowExit; - this.players = context.players.map(p => new PlayerShapshot(p)); - this.servers = context.servers.map(s => new ServerShapshot(s)); + this.nodes = context.nodes.map((n: INode) => new NodeSnapshot(n)); } } diff --git a/js/sdk-core/src/game-context.ts b/js/sdk-core/src/game-context.ts index 7ac6adea..c337069b 100644 --- a/js/sdk-core/src/game-context.ts +++ b/js/sdk-core/src/game-context.ts @@ -1,5 +1,6 @@ import { RandomState, RandomSpec } from './random-state'; import { DecisionState } from './decision-state'; +import { Checkpoint } from './checkpoint'; import { ActionTimeout, Answer, @@ -13,9 +14,12 @@ import { Shutdown, WaitingTimeout, } from './events'; -import { Effect, Settle, SettleAdd, SettleEject, SettleSub, Transfer } from './effect'; -import { GameAccount, PlayerJoin, ServerJoin } from './accounts'; +import { GamePlayer, InitAccount } from './init-account'; +import { Effect, EmitBridgeEvent, SubGame, Settle, Transfer, SettleAdd, SettleSub, SettleEject } from './effect'; +import { EntryType, GameAccount } from './accounts'; import { Ciphertext, Digest, Id } from './types'; +import { clone } from './utils'; +import rfdc from 'rfdc'; const OPERATION_TIMEOUT = 15_000n; @@ -27,19 +31,15 @@ export type NodeStatus = | { kind: 'ready' } | { kind: 'disconnected' }; -export type GameStatus = 'uninit' | 'running' | 'closed'; +export type ClientMode = 'player' | 'transactor' | 'validator'; -export interface IPlayer { - addr: string; - position: number; - balance: bigint; - status: NodeStatus; -} +export type GameStatus = 'idle' | 'running' | 'closed'; -export interface IServer { +export interface INode { addr: string; + id: bigint; + mode: ClientMode; status: NodeStatus; - endpoint: string; } export interface DispatchEvent { @@ -47,97 +47,169 @@ export interface DispatchEvent { event: GameEvent; } +export interface IdAddrPair { + id: bigint; + addr: string; +} + +export type EventEffects = { + settles: Settle[]; + transfers: Transfer[]; + checkpoint: Uint8Array | undefined; + launchSubGames: SubGame[]; + bridgeEvents: EmitBridgeEvent[]; + startGame: boolean; +} + export class GameContext { gameAddr: string; + gameId: number; accessVersion: bigint; settleVersion: bigint; - transactorAddr: string; status: GameStatus; - players: IPlayer[]; - servers: IServer[]; + nodes: INode[]; dispatch: DispatchEvent | undefined; handlerState: Uint8Array; timestamp: bigint; allowExit: boolean; randomStates: RandomState[]; decisionStates: DecisionState[]; - settles: Settle[] | undefined; - transfers: Transfer[] | undefined; - checkpoint: Uint8Array | undefined; - checkpointAccessVersion: bigint; - - constructor(context: GameContext); - constructor(gameAccount: GameAccount); - constructor(gameAccountOrContext: GameAccount | GameContext) { - if (gameAccountOrContext instanceof GameContext) { - const context = gameAccountOrContext; - this.gameAddr = context.gameAddr; - this.accessVersion = context.accessVersion; - this.settleVersion = context.settleVersion; - this.transactorAddr = context.transactorAddr; - this.status = context.status; - this.players = context.players.map(p => Object.assign({}, p)); - this.servers = context.servers.map(s => Object.assign({}, s)); - this.dispatch = context.dispatch; - this.handlerState = new Uint8Array(context.handlerState); - this.timestamp = context.timestamp; - this.allowExit = context.allowExit; - this.randomStates = context.randomStates; - this.decisionStates = context.decisionStates; - this.settles = context.settles; - this.transfers = context.transfers; - this.checkpoint = undefined; - this.checkpointAccessVersion = context.checkpointAccessVersion; - } else { - const gameAccount = gameAccountOrContext; + checkpoint: Checkpoint; + subGames: SubGame[]; + initData: Uint8Array; + maxPlayers: number; + players: GamePlayer[]; + entryType: EntryType; + + constructor(gameAccount: GameAccount, checkpoint: Checkpoint) { + if (checkpoint === undefined) { + throw new Error('Missing checkpoint'); + } + console.log('Build game context with checkpoint:', clone(checkpoint)); + const checkpointAccessVersion = gameAccount.checkpointOnChain?.accessVersion || 0; const transactorAddr = gameAccount.transactorAddr; if (transactorAddr === undefined) { throw new Error('Game not served'); } - const players: IPlayer[] = gameAccount.players.map(p => ({ - addr: p.addr, - balance: p.balance, - position: p.position, - status: { - kind: 'pending', - accessVersion: p.accessVersion, - }, - })); - const servers: IServer[] = gameAccount.servers.map(s => ({ + let nodes: INode[] = []; + gameAccount.servers.forEach(s => nodes.push({ addr: s.addr, - endpoint: s.endpoint, - status: { - kind: 'pending', - accessVersion: s.accessVersion, - }, + id: s.accessVersion, + mode: s.addr === transactorAddr ? 'transactor' : 'validator', + status: s.addr === gameAccount.transactorAddr + ? { kind: 'ready' } + : { + kind: 'pending', + accessVersion: s.accessVersion, + }, })); + gameAccount.players.forEach(p => nodes.push({ + addr: p.addr, + id: p.accessVersion, + mode: 'player', + status: p.addr === gameAccount.transactorAddr + ? { kind: 'ready' } + : { + kind: 'pending', + accessVersion: p.accessVersion, + }, + })) + + const players = gameAccount.players + .filter(p => p.accessVersion <= checkpointAccessVersion) + .map(p => new GamePlayer({ + balance: p.balance, + id: p.accessVersion, + position: p.position + })) this.gameAddr = gameAccount.addr; - this.transactorAddr = transactorAddr; + this.gameId = 0; this.accessVersion = gameAccount.accessVersion; this.settleVersion = gameAccount.settleVersion; - this.status = 'uninit'; + this.status = 'idle'; this.dispatch = undefined; - this.players = players; - this.servers = servers; + this.nodes = nodes; this.timestamp = 0n; this.allowExit = false; this.randomStates = []; this.decisionStates = []; - this.settles = undefined; - this.transfers = undefined; this.handlerState = Uint8Array.of(); - this.checkpoint = undefined; - this.checkpointAccessVersion = gameAccount.checkpointAccessVersion; + this.checkpoint = checkpoint; + this.subGames = []; + this.initData = gameAccount.data; + this.maxPlayers = gameAccount.maxPlayers; + this.players = players; + this.entryType = gameAccount.entryType; } + + subContext(subGame: SubGame): GameContext { + const c = rfdc({ proto: true })(this); + Object.setPrototypeOf(c, GameContext.prototype); + c.accessVersion = c.accessVersion; + c.settleVersion = c.settleVersion; + c.gameAddr = c.gameAddr + subGame.gameId; + c.gameId = subGame.gameId; + c.dispatch = undefined; + c.timestamp = 0n; + c.allowExit = false; + c.randomStates = []; + c.decisionStates = []; + c.handlerState = Uint8Array.of(); + c.checkpoint = this.checkpoint.clone(); + c.subGames = []; + c.initData = subGame.initAccount.data; + c.maxPlayers = subGame.initAccount.maxPlayers; + c.entryType = subGame.initAccount.entryType; + c.players = subGame.initAccount.players.map(p => new GamePlayer(p)); + return c; + } + + checkpointVersion(): bigint { + return this.checkpoint.getVersion(this.gameId) + } + + initAccount(): InitAccount { + const checkpoint = this.checkpoint.getData(this.gameId); + return new InitAccount({ + maxPlayers: this.maxPlayers, + players: this.players.map(p => new GamePlayer(p)), + entryType: this.entryType, + data: this.initData, + checkpoint, + }); } - getServerByAddress(addr: string): IServer | undefined { - return this.servers.find(s => s.addr === addr); + // get checkpointStateSha(): string { + // return this.checkpoint.getSha(this.gameId) || ''; + // } + + idToAddrUnchecked(id: bigint): string | undefined { + return this.nodes.find(x => x.id === id)?.addr; + } + + idToAddr(id: bigint): string { + let found = this.idToAddrUnchecked(id); + if (found === undefined) { + throw new Error(`Cannot map id to address: ${id.toString()}`); + } + return found; } - getPlayerByAddress(addr: string): IPlayer | undefined { - return this.players.find(p => p.addr === addr); + addrToIdUnchecked(addr: string): bigint | undefined { + return this.nodes.find(x => x.addr === addr)?.id; + } + + addrToId(addr: string): bigint { + let found = this.addrToIdUnchecked(addr); + if (found === undefined) { + throw new Error(`Cannot map address to id: ${addr}`); + } + return found; + } + + getNodeByAddress(addr: string): INode | undefined { + return this.nodes.find(n => n.addr === addr); } dispatchEvent(event: GameEvent, timeout: bigint) { @@ -158,9 +230,9 @@ export class GameContext { }; } - actionTimeout(playerAddr: string, timeout: bigint) { + actionTimeout(playerId: bigint, timeout: bigint) { this.dispatch = { - event: new ActionTimeout({ playerAddr }), + event: new ActionTimeout({ playerId }), timeout: this.timestamp + timeout, }; } @@ -234,43 +306,22 @@ export class GameContext { return this.randomStates.every(st => st.status.kind === 'ready'); } - setPlayerStatus(addr: string, status: NodeStatus) { - let p = this.players.find(p => p.addr === addr); - if (p === undefined) { - throw new Error('Invalid player address'); - } - p.status = status; - } - - addPlayer(player: PlayerJoin) { - const exist = this.players.find(p => p.addr === player.addr || p.position === player.position); - if (exist === undefined) { - this.players.push({ - addr: player.addr, - balance: player.balance, - status: { kind: 'ready' }, - position: player.position, - }); - } else { - if (exist.position === player.position) { - throw new Error('Position occupied'); - } else { - throw new Error('Player already joined'); - } + setNodeStatus(addr: string, status: NodeStatus) { + let n = this.nodes.find(n => n.addr === addr); + if (n === undefined) { + throw new Error('Invalid node address'); } + n.status = status; } - addServer(server: ServerJoin) { - const exist = this.players.find(s => s.addr === server.addr); - if (exist === undefined) { - this.servers.push({ - addr: server.addr, - status: { kind: 'ready' }, - endpoint: server.endpoint, - }); - } else { - throw new Error('Server already joined'); - } + addNode(nodeAddr: string, accessVersion: bigint, mode: ClientMode) { + this.nodes = this.nodes.filter(n => n.addr !== nodeAddr); + this.nodes.push({ + addr: nodeAddr, + id: accessVersion, + mode, + status: { kind: 'pending', accessVersion } + }); } setAccessVersion(accessVersion: bigint) { @@ -281,21 +332,9 @@ export class GameContext { this.allowExit = allowExit; } - removePlayer(addr: string) { - if (this.allowExit) { - const origLen = this.players.length; - this.players = this.players.filter(p => p.addr !== addr); - if (this.players.length === origLen) { - throw new Error('Player not in game'); - } - } else { - throw new Error("Can't leave"); - } - } - initRandomState(spec: RandomSpec): Id { const randomId = this.randomStates.length + 1; - const owners = this.servers.filter(s => s.status.kind === 'ready').map(s => s.addr); + const owners = this.nodes.filter(n => n.status.kind === 'ready' && n.mode !== 'player').map(n => n.addr); const randomState = new RandomState(randomId, spec, owners); this.randomStates.push(randomState); return randomId; @@ -333,70 +372,22 @@ export class GameContext { this.dispatchEventInstantly(new RandomnessReady({ randomId })); } else if (statusKind === 'locking' || statusKind === 'masking') { const addr = st.status.addr; + const id = this.addrToId(addr); if (noDispatch) { - this.dispatchEvent(new OperationTimeout({ addrs: [addr] }), OPERATION_TIMEOUT); + this.dispatchEvent(new OperationTimeout({ ids: [id] }), OPERATION_TIMEOUT); } } else if (statusKind === 'waiting-secrets') { if (noDispatch) { - const addrs = st.listOperatingAddrs(); - this.dispatchEvent(new OperationTimeout({ addrs }), OPERATION_TIMEOUT); + const ids = st.listOperatingAddrs().map(x => this.addrToId(x)); + this.dispatchEvent(new OperationTimeout({ ids }), OPERATION_TIMEOUT); } } } - settle(settles: Settle[]) { - this.settles = settles; - } - - transfer(transfers: Transfer[]) { - this.transfers = transfers; - } - bumpSettleVersion() { this.settleVersion += 1n; } - /* - This function refers to the backend function `take_settles_and_transfers`. - Here, we don't have to deal with transfers before we introducing settlement validation. - */ - applyAndTakeSettles(): Settle[] | undefined { - if (this.settles === undefined) { - return undefined; - } - let settles = this.settles; - this.settles = undefined; - settles = settles.sort((s1, s2) => s1.compare(s2)); - for (const s of settles) { - if (s.op instanceof SettleAdd) { - let p = this.getPlayerByAddress(s.addr); - if (p === undefined) { - throw new Error('Invalid settle player address'); - } - p.balance += s.op.amount; - } else if (s.op instanceof SettleSub) { - let p = this.getPlayerByAddress(s.addr); - if (p === undefined) { - throw new Error('Invalid settle player address'); - } - p.balance -= s.op.amount; - } else if (s.op instanceof SettleEject) { - this.players = this.players.filter(p => p.addr !== s.addr); - } - } - - this.bumpSettleVersion(); - return settles; - } - - addSettle(settle: Settle) { - if (this.settles === undefined) { - this.settles = [settle]; - } else { - this.settles.push(settle); - } - } - addRevealedRandom(randomId: Id, revealed: Map) { const st = this.getRandomState(randomId); st.addRevealed(revealed); @@ -424,13 +415,13 @@ export class GameContext { return st.revealed; } - applyEffect(effect: Effect) { + async applyEffect(effect: Effect): Promise { if (effect.startGame) { this.startGame(); } else if (effect.stopGame) { this.shutdownGame(); } else if (effect.actionTimeout !== undefined) { - this.actionTimeout(effect.actionTimeout.playerAddr, effect.actionTimeout.timeout); + this.actionTimeout(effect.actionTimeout.playerId, effect.actionTimeout.timeout); } else if (effect.waitTimeout !== undefined) { this.waitTimeout(effect.waitTimeout); } else if (effect.cancelDispatch) { @@ -438,7 +429,8 @@ export class GameContext { } this.setAllowExit(effect.allowExit); for (const assign of effect.assigns) { - this.assign(assign.randomId, assign.playerAddr, assign.indexes); + const addr = this.idToAddr(assign.playerId); + this.assign(assign.randomId, addr, assign.indexes); } for (const reveal of effect.reveals) { this.reveal(reveal.randomId, reveal.indexes); @@ -449,56 +441,109 @@ export class GameContext { for (const spec of effect.initRandomStates) { this.initRandomState(spec); } - if (effect.isCheckpoint) { - this.settle(effect.settles); - this.transfer(effect.transfers); - this.checkpoint = effect.checkpoint; - } + + let settles: Settle[] = []; + if (effect.handlerState !== undefined) { this.handlerState = effect.handlerState; + if (effect.isCheckpoint) { + this.randomStates = []; + this.decisionStates = []; + this.bumpSettleVersion(); + this.checkpoint.setData(this.gameId, effect.handlerState); + this.checkpoint.setAccessVersion(this.accessVersion); + + // Reset random states + this.randomStates = []; + this.decisionStates = []; + + // Sort settles and track player states + settles.push(...effect.settles); + settles = effect.settles; + settles = settles.sort((s1, s2) => s1.compare(s2)); + for (let s of settles) { + if (s.op instanceof SettleAdd) { + this.playerAddBalance(s.id, s.op.amount); + } else if (s.op instanceof SettleSub) { + this.playerSubBalance(s.id, s.op.amount); + } else if (s.op instanceof SettleEject) { + this.removePlayer(s.id); + } + } + } + } + + for (const subGame of effect.launchSubGames) { + this.addSubGame(subGame); } + + return { + checkpoint: effect.isCheckpoint ? effect.handlerState : undefined, + settles, + transfers: effect.transfers, + startGame: effect.startGame, + launchSubGames: effect.launchSubGames, + bridgeEvents: effect.bridgeEvents, + }; } setNodeReady(accessVersion: bigint) { - for (const s of this.servers) { - if (s.status.kind === 'pending') { - if (s.status.accessVersion < accessVersion) { - s.status = { kind: 'ready' }; - } - } - } - for (const p of this.players) { - if (p.status.kind === 'pending') { - if (p.status.accessVersion < accessVersion) { - p.status = { kind: 'ready' }; + for (const n of this.nodes) { + if (n.status.kind === 'pending') { + if (n.status.accessVersion <= accessVersion) { + console.debug(`Set node ${n.addr} status to ready`); + n.status = { kind: 'ready' }; } } } } applyCheckpoint(accessVersion: bigint, settleVersion: bigint) { + console.log(`Apply checkpoint, accessVersion: ${accessVersion}`) if (this.settleVersion !== settleVersion) { throw new Error(`Invalid checkpoint, local settle version: ${this.settleVersion}, remote settle version: ${settleVersion}`); } - this.players = this.players.filter(p => { - if (p.status.kind === 'pending') { - return p.status.accessVersion <= accessVersion; - } else { - return true; - } - }); - this.servers = this.servers.filter(s => { - if (s.status.kind === 'pending') { - return s.status.accessVersion <= accessVersion || s.addr === this.transactorAddr; - } else { - return true; - } - }); this.accessVersion = accessVersion; } - prepareForNextEvent(timestamp: bigint) { + setTimestamp(timestamp: bigint) { this.timestamp = timestamp; - this.checkpoint = undefined; + } + + findSubGame(gameId: number): SubGame | undefined { + return this.subGames.find(g => g.gameId === Number(gameId)); + } + + addSubGame(subGame: SubGame) { + const found = this.subGames.find(s => s.gameId === subGame.gameId); + if (found === undefined) { + this.subGames.push(subGame); + } else { + found.initAccount = subGame.initAccount; + } + } + + addPlayer(player: GamePlayer) { + this.players.push(player); + } + + removePlayer(playerId: bigint) { + this.players = this.players.filter(p => p.id !== playerId); + } + + playerAddBalance(playerId: bigint, amount: bigint) { + let p = this.players.find(p => p.id === playerId); + if (p === undefined) { + throw new Error(`Player not in game: ${playerId}`) + } + p.balance = p.balance + amount; + } + + playerSubBalance(playerId: bigint, amount: bigint) { + let p = this.players.find(p => p.id === playerId); + if (p === undefined) { + throw new Error(`Player not in game: ${playerId}`) + } + p.balance = p.balance - amount; } } diff --git a/js/sdk-core/src/handler.ts b/js/sdk-core/src/handler.ts index 619e94ec..4f382e25 100644 --- a/js/sdk-core/src/handler.ts +++ b/js/sdk-core/src/handler.ts @@ -1,85 +1,17 @@ -import { array, deserialize, field, serialize, struct } from '@race-foundation/borsh'; -import { GameAccount, GameBundle, PlayerJoin, ServerJoin } from './accounts'; -import { AnswerDecision, GameEvent, GameStart, Leave, Mask, Lock, SecretsReady, ShareSecrets, Sync } from './events'; -import { GameContext } from './game-context'; +import { deserialize, serialize } from '@race-foundation/borsh'; +import { GameBundle } from './accounts'; +import { AnswerDecision, GameEvent, GameStart, Leave, Mask, Lock, SecretsReady, ShareSecrets, Join, Bridge } from './events'; +import { EventEffects, GameContext } from './game-context'; import { IEncryptor } from './encryptor'; import { Effect } from './effect'; import { Client } from './client'; import { DecryptionCache } from './decryption-cache'; - -/** - * A subset of GameAccount, used in handler initialization. - */ -export interface IInitAccount { - addr: string; - players: PlayerJoin[]; - servers: ServerJoin[]; - data: Uint8Array; - accessVersion: bigint; - settleVersion: bigint; - maxPlayers: number; - checkpoint: Uint8Array; -} - -export class InitAccount { - @field('string') - readonly addr: string; - @field(array(struct(PlayerJoin))) - readonly players: PlayerJoin[]; - @field(array(struct(ServerJoin))) - readonly servers: ServerJoin[]; - @field('u8-array') - readonly data: Uint8Array; - @field('u64') - readonly accessVersion: bigint; - @field('u64') - readonly settleVersion: bigint; - @field('u16') - readonly maxPlayers: number; - @field('u8-array') - readonly checkpoint: Uint8Array; - - constructor(fields: IInitAccount) { - this.addr = fields.addr; - this.accessVersion = fields.accessVersion; - this.settleVersion = fields.settleVersion; - this.data = fields.data; - this.players = fields.players; - this.servers = fields.servers; - this.maxPlayers = fields.maxPlayers; - this.checkpoint = fields.checkpoint; - } - static createFromGameAccount( - gameAccount: GameAccount, - transactorAccessVersion: bigint, - transactorSettleVersion: bigint - ): InitAccount { - let { addr, players, servers, data, checkpointAccessVersion, transactorAddr } = gameAccount; - players = players.filter(p => p.accessVersion <= checkpointAccessVersion); - servers = servers.filter(s => s.accessVersion <= checkpointAccessVersion || s.addr === transactorAddr); - return new InitAccount({ - addr, - data, - players, - servers, - accessVersion: transactorAccessVersion, - settleVersion: transactorSettleVersion, - maxPlayers: gameAccount.maxPlayers, - checkpoint: gameAccount.checkpoint, - }); - } - serialize(): Uint8Array { - return serialize(InitAccount); - } - static deserialize(data: Uint8Array) { - return deserialize(InitAccount, data); - } -} +import { InitAccount } from './init-account'; export interface IHandler { - handleEvent(context: GameContext, event: GameEvent): Promise; + handleEvent(context: GameContext, event: GameEvent): Promise; - initState(context: GameContext, initAccount: InitAccount): Promise; + initState(context: GameContext, initAccount: InitAccount): Promise; } export class Handler implements IHandler { @@ -115,27 +47,28 @@ export class Handler implements IHandler { return new Handler(initiatedSource.instance, encryptor, client, decryptionCache); } - async handleEvent(context: GameContext, event: GameEvent) { + async handleEvent(context: GameContext, event: GameEvent): Promise { await this.generalPreHandleEvent(context, event, this.#encryptor); - await this.customHandleEvent(context, event); - await this.generalPostHandleEvent(context, event); - context.applyAndTakeSettles(); + return await this.customHandleEvent(context, event); } - async initState(context: GameContext, initAccount: InitAccount) { + async initState(context: GameContext): Promise { + const initAccount = context.initAccount(); + console.log('InitState with:', initAccount); + context.setTimestamp(0n); // Use 0 timestamp for initState await this.generalPreInitState(context, initAccount); - await this.customInitState(context, initAccount); - await this.generalPostInitState(context, initAccount); + return await this.customInitState(context, initAccount); } - async generalPreInitState(_context: GameContext, _initAccount: InitAccount) { } - - async generalPostInitState(_context: GameContext, _initAccount: InitAccount) { } + async generalPreInitState(context: GameContext, _initAccount: InitAccount) { + context.dispatch = undefined; + } async generalPreHandleEvent(context: GameContext, event: GameEvent, encryptor: IEncryptor) { if (event instanceof ShareSecrets) { const { sender, shares } = event; - context.addSharedSecrets(sender, shares); + const addr = context.idToAddr(sender); + context.addSharedSecrets(addr, shares); let randomIds: number[] = []; for (let randomState of context.randomStates) { if (randomState.status.kind === 'shared') { @@ -143,43 +76,31 @@ export class Handler implements IHandler { randomState.status = { kind: 'ready' }; } } - if (randomIds.length > 0) { context.dispatchEventInstantly(new SecretsReady({ randomIds })); } } else if (event instanceof AnswerDecision) { const { decisionId, ciphertext, sender, digest } = event; - context.answerDecision(decisionId, sender, ciphertext, digest); + const addr = context.idToAddr(sender); + context.answerDecision(decisionId, addr, ciphertext, digest); } else if (event instanceof Mask) { const { sender, randomId, ciphertexts } = event; - context.randomizeAndMask(sender, randomId, ciphertexts); + const addr = context.idToAddr(sender); + context.randomizeAndMask(addr, randomId, ciphertexts); } else if (event instanceof Lock) { const { sender, randomId, ciphertextsAndDigests } = event; - context.lock(sender, randomId, ciphertextsAndDigests); - } else if (event instanceof Sync) { - const { accessVersion, newPlayers, newServers } = event; - if (accessVersion < context.accessVersion) { - throw new Error('Event ignored'); - } - for (const p of newPlayers) { - context.addPlayer(p); - } - for (const s of newServers) { - context.addServer(s); - } - context.accessVersion = accessVersion; + const addr = context.idToAddr(sender); + context.lock(addr, randomId, ciphertextsAndDigests); + } else if (event instanceof Join) { + event.players.forEach(p => context.addPlayer(p)); } else if (event instanceof Leave) { - const { playerAddr } = event; - const exist = context.players.find(p => p.addr === playerAddr); - if (exist === undefined) { - throw new Error('Invalid player address'); + if (!context.allowExit) { + throw new Error('Leave is not allowed') } } else if (event instanceof GameStart) { - const { accessVersion } = event; context.status = 'running'; - context.setNodeReady(accessVersion); + context.setNodeReady(context.accessVersion); } else if (event instanceof SecretsReady) { - for (let randomId of event.randomIds) { let decryption = await this.#client.decrypt(context, randomId); this.#decryptionCache.add(randomId, decryption); @@ -194,23 +115,20 @@ export class Handler implements IHandler { ); context.addRevealedRandom(st.id, revealed); } + } else if (event instanceof Bridge) { + event.joinPlayers.forEach(p => context.addPlayer(p)); } } - async generalPostHandleEvent(context: GameContext, event: GameEvent) { - if (context.checkpoint) { - context.randomStates = []; - context.decisionStates = []; - } - } - - async customInitState(context: GameContext, initAccount: InitAccount) { + async customInitState(context: GameContext, initAccount: InitAccount): Promise { const exports = this.#instance.exports; const mem = exports.memory as WebAssembly.Memory; mem.grow(4); let buf = new Uint8Array(mem.buffer); - const effect = Effect.fromContext(context); + const effect = Effect.fromContext(context, true); + console.log('Effect:', effect); + const effectBytes = serialize(effect); const effectSize = effectBytes.length; @@ -240,26 +158,24 @@ export class Handler implements IHandler { console.error(newEffect.error); throw newEffect.error; } else { - context.applyEffect(newEffect); + return await context.applyEffect(newEffect); } } - async customHandleEvent(context: GameContext, event: GameEvent) { + async customHandleEvent(context: GameContext, event: GameEvent): Promise { const exports = this.#instance.exports; const mem = exports.memory as WebAssembly.Memory; let buf = new Uint8Array(mem.buffer); - const effect = Effect.fromContext(context); - // console.debug("Effect before ser: ", effect); + const effect = Effect.fromContext(context, false); + + console.log('Effect:', effect); const effectBytes = serialize(effect); const effectSize = effectBytes.length; const eventBytes = serialize(event); const eventSize = eventBytes.length; - // console.debug("Event Bytes: [%s]", Array.of(eventBytes).toString()); - // console.debug("Effect Bytes: [%s]", Array.of(effectBytes).toString()); - if (buf.length < 1 + eventSize + effectSize) { throw new Error(`WASM memory overflow, buffer length: ${buf.length}, required: ${1 + eventSize + effectSize}`); } @@ -294,7 +210,7 @@ export class Handler implements IHandler { if (newEffect.error !== undefined) { throw newEffect.error; } else { - context.applyEffect(newEffect); + return await context.applyEffect(newEffect); } } } diff --git a/js/sdk-core/src/index.ts b/js/sdk-core/src/index.ts index 61540433..39b506ca 100644 --- a/js/sdk-core/src/index.ts +++ b/js/sdk-core/src/index.ts @@ -7,3 +7,8 @@ export * from './events'; export * from './game-context-snapshot'; export * from './connection'; export * from './storage'; +export * from './sub-client'; +export * from './types'; +export * from './checkpoint'; + +console.log('Race Protocol 24.04'); diff --git a/js/sdk-core/src/init-account.ts b/js/sdk-core/src/init-account.ts new file mode 100644 index 00000000..7a71fe9a --- /dev/null +++ b/js/sdk-core/src/init-account.ts @@ -0,0 +1,54 @@ +import { array, deserialize, enums, field, option, serialize, struct } from "@race-foundation/borsh"; +import { EntryType, GameAccount } from "./accounts"; +import { Fields } from "./types"; + +export class GamePlayer { + @field('u64') + id!: bigint; + @field('u16') + position!: number; + @field('u64') + balance!: bigint; + constructor(fields: Fields) { + Object.assign(this, fields) + } +} + +/** + * A subset of GameAccount, used in handler initialization. + */ +export interface IInitAccount { + maxPlayers: number; + entryType: EntryType; + players: GamePlayer[]; + data: Uint8Array; + checkpoint: Uint8Array | undefined; +} + +export class InitAccount { + @field('u16') + readonly maxPlayers: number; + @field(enums(EntryType)) + readonly entryType: EntryType; + @field(array(struct(GamePlayer))) + readonly players: GamePlayer[]; + @field('u8-array') + readonly data: Uint8Array; + @field(option('u8-array')) + readonly checkpoint: Uint8Array | undefined; + + constructor(fields: IInitAccount) { + this.maxPlayers = fields.maxPlayers; + this.entryType = fields.entryType; + this.players = fields.players; + this.data = fields.data; + this.checkpoint = fields.checkpoint; + } + + serialize(): Uint8Array { + return serialize(InitAccount); + } + static deserialize(data: Uint8Array) { + return deserialize(InitAccount, data); + } +} diff --git a/js/sdk-core/src/profile-cache.ts b/js/sdk-core/src/profile-cache.ts deleted file mode 100644 index 68d12a41..00000000 --- a/js/sdk-core/src/profile-cache.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PlayerProfile } from './accounts'; -import { GameContextSnapshot } from './game-context-snapshot'; -import { ITransport } from './transport'; - -export class ProfileCache { - transport: ITransport; - caches: Map; - - constructor(transport: ITransport) { - this.transport = transport; - this.caches = new Map(); - } - - async getProfile(playerAddr: string): Promise { - let exist = this.caches.get(playerAddr); - if (exist !== undefined) { - return exist; - } else { - const p = await this.transport.getPlayerProfile(playerAddr); - if (p === undefined) { - console.warn('Failed to fetch the profile of %s', playerAddr); - return undefined; - } else { - this.caches.set(playerAddr, p); - return p; - } - } - } - - async injectProfiles(context: GameContextSnapshot) { - for (let player of context.players) { - if (player.profile === undefined) { - const profile = await this.getProfile(player.addr); - player.profile = profile; - } - } - } -} diff --git a/js/sdk-core/src/profile-loader.ts b/js/sdk-core/src/profile-loader.ts new file mode 100644 index 00000000..1773a881 --- /dev/null +++ b/js/sdk-core/src/profile-loader.ts @@ -0,0 +1,98 @@ +import { IStorage } from './storage'; +import { ITransport } from './transport'; +import { PlayerProfileWithPfp, ProfileCallbackFunction } from './types'; + +type LoadingStatus = 'loading' | 'loaded' | 'failed'; + +export class ProfileLoader { + transport: ITransport; + caches: Map; + loadingStatus: Map; + storage?: IStorage; + shutdown: boolean; + onProfile: ProfileCallbackFunction; + addrToId: Map; + + constructor(transport: ITransport, storage: IStorage | undefined, onProfile: ProfileCallbackFunction) { + this.transport = transport; + this.storage = storage; + this.caches = new Map(); + this.loadingStatus = new Map(); + this.shutdown = false; + this.onProfile = onProfile; + this.addrToId = new Map(); + } + + async __loadProfile(playerAddr: string): Promise { + const profile = await this.transport.getPlayerProfile(playerAddr); + if (profile === undefined) { + return undefined; + } else { + let p; + if (profile.pfp !== undefined) { + let pfp = await this.transport.getNft(profile.pfp, this.storage); + p = { pfp, addr: profile.addr, nick: profile.nick }; + } else { + p = { pfp: undefined, addr: profile.addr, nick: profile.nick }; + } + console.debug(`Load profile, address = ${playerAddr}`, profile); + return p; + } + } + + getProfile(playerAddr: string): PlayerProfileWithPfp | undefined { + return this.caches.get(playerAddr); + } + + async start() { + while (true) { + if (this.shutdown) { + break; + } + for (const [addr, s] of this.loadingStatus) { + if (s === 'loading') { + const p = await this.__loadProfile(addr); + if (p === undefined) { + this.loadingStatus.set(addr, 'failed'); + } else { + if (this.onProfile !== undefined) { + const id = this.addrToId.get(p.addr); + if (id === undefined) { + console.warn(`Cannot find the mapping from address = ${p.addr} to id, available mapping:`, this.addrToId); + throw new Error('Cannot find the mapping from address to id'); + } + this.onProfile(id, p); + } + this.caches.set(addr, p); + this.loadingStatus.set(addr, 'loaded'); + } + } + } + await new Promise(r => setTimeout(r, 1000)); + } + } + + load(id: bigint, addr: string) { + const status = this.loadingStatus.get(addr); + this.addrToId.set(addr, id); + if (status === undefined) { + console.debug(`Load profile: ${addr}, this is the first loading for this address`); + this.loadingStatus.set(addr, 'loading'); + } else if (status === 'failed') { + console.debug(`Load profile: ${addr}, this is a reloading after a failure`); + this.loadingStatus.set(addr, 'loading'); + } else if (status === 'loaded') { + console.debug(`Load profile: ${addr}, get the result from cache`); + const p = this.caches.get(addr); + if (p !== undefined) { + this.onProfile(id, p); + } else { + console.error(`Unexpected profile cache not found, address: ${addr}`); + } + } + } + + stop() { + this.shutdown = true; + } +} diff --git a/js/sdk-core/src/sub-client.ts b/js/sdk-core/src/sub-client.ts new file mode 100644 index 00000000..39488627 --- /dev/null +++ b/js/sdk-core/src/sub-client.ts @@ -0,0 +1,64 @@ +import { BaseClient } from './base-client'; +import { Client } from './client'; +import { IConnection, SubscribeEventParams } from './connection'; +import { DecryptionCache } from './decryption-cache'; +import { IEncryptor } from './encryptor'; +import { GameContext } from './game-context'; +import { Handler } from './handler'; +import { ITransport } from './transport'; +import { GameInfo, ConnectionStateCallbackFunction, EventCallbackFunction, MessageCallbackFunction, TxStateCallbackFunction, ErrorCallbackFunction } from './types'; +import { IWallet } from './wallet'; +import { Init } from './events'; +import { CheckpointOnChain } from './checkpoint'; +import { clone } from './utils'; + +export type SubClientCtorOpts = { + gameAddr: string; + gameId: number; + handler: Handler; + wallet: IWallet; + client: Client; + transport: ITransport; + connection: IConnection; + gameContext: GameContext; + latestCheckpointOnChain: CheckpointOnChain | undefined; + onEvent: EventCallbackFunction; + onMessage: MessageCallbackFunction | undefined; + onTxState: TxStateCallbackFunction | undefined; + onConnectionState: ConnectionStateCallbackFunction | undefined; + onError: ErrorCallbackFunction | undefined; + encryptor: IEncryptor; + info: GameInfo; + decryptionCache: DecryptionCache; +} + +export class SubClient extends BaseClient { + constructor(opts: SubClientCtorOpts) { + super({ + onLoadProfile: (_id: bigint, _addr: string) => {}, + logPrefix: `SubGame#${opts.gameId}|`, + ...opts + }) + } + + /** + * Connect to the transactor and retrieve the event stream. + */ + async attachGame() { + console.group(`${this.__logPrefix}Attach to game`); + let sub; + try { + console.log('Checkpoint:', clone(this.__gameContext.checkpoint)); + await this.__attachGameWithRetry(); + sub = this.__connection.subscribeEvents(); + const settleVersion = this.__gameContext.checkpointVersion(); + await this.__connection.connect(new SubscribeEventParams({ settleVersion })); + } catch (e) { + console.error('Attaching game failed', e); + throw e; + } finally { + console.groupEnd(); + } + await this.__processSubscription(sub); + } +} diff --git a/js/sdk-core/src/transport.ts b/js/sdk-core/src/transport.ts index 94978fee..052e4a14 100644 --- a/js/sdk-core/src/transport.ts +++ b/js/sdk-core/src/transport.ts @@ -11,6 +11,7 @@ import { RegistrationWithGames, RecipientAccount, EntryType, + ITokenWithBalance, } from './accounts'; import { IStorage } from './storage'; @@ -35,6 +36,8 @@ export type CreateGameAccountParams = { tokenAddr: string; maxPlayers: number; entryType: EntryType; + recipientAddr: string; + registrationAddr: string; data: Uint8Array; }; @@ -45,10 +48,9 @@ export type CloseGameAccountParams = { export type JoinParams = { gameAddr: string; amount: bigint; - accessVersion: bigint; position: number; verifyKey: string; - createProfile?: boolean; + createProfileIfNeeded?: boolean; }; export type DepositParams = { @@ -136,11 +138,11 @@ export interface ITransport { getNft(addr: string, storage?: IStorage): Promise; - listTokens(storage?: IStorage): Promise; + listTokens(tokenAddrs: string[], storage?: IStorage): Promise; - listNfts(walletAddr: string, storage?: IStorage): Promise; + listTokensWithBalance(walletAddr: string, tokenAddrs: string[], storage?: IStorage): Promise; - fetchBalances(walletAddr: string, tokenAddrs: string[]): Promise>; + listNfts(walletAddr: string, storage?: IStorage): Promise; recipientClaim(wallet: IWallet, params: RecipientClaimParams): Promise>; } diff --git a/js/sdk-core/src/tx-state.ts b/js/sdk-core/src/tx-state.ts index 46dcf6f3..893a6dbb 100644 --- a/js/sdk-core/src/tx-state.ts +++ b/js/sdk-core/src/tx-state.ts @@ -1,13 +1,35 @@ -import { field, array, enums, option, variant, struct } from '@race-foundation/borsh'; -import { PlayerJoin } from './accounts'; +import { field, array, variant, struct, option } from '@race-foundation/borsh'; -// type StateField = Omit, 'kind'>; export abstract class TxState {} +export type TxStateKind = + | 'PlayerConfirming' + | 'PlayerConfirmingFailed' + | 'SettleSucceed'; + +export interface ITxStateKind { + kind(): TxStateKind; +} + +export class ConfirmingPlayer { + @field('u64') + id!: bigint; + @field('string') + addr!: string; + @field('u16') + position!: number; + @field('u64') + balance!: bigint; + + constructor(fields: any) { + Object.assign(this, fields); + } +} + @variant(0) -export class PlayerConfirming extends TxState { - @field(array(struct(PlayerJoin))) - confirmPlayers!: PlayerJoin[]; +export class PlayerConfirming extends TxState implements ITxStateKind { + @field(array(struct(ConfirmingPlayer))) + confirmPlayers!: ConfirmingPlayer[]; @field('u64') accessVersion!: bigint; @@ -15,10 +37,13 @@ export class PlayerConfirming extends TxState { super(); Object.assign(this, fields); } + kind(): TxStateKind { + return 'PlayerConfirming'; + } } @variant(1) -export class PlayerConfirmingFailed extends TxState { +export class PlayerConfirmingFailed extends TxState implements ITxStateKind { @field('u64') accessVersion!: bigint; @@ -26,4 +51,23 @@ export class PlayerConfirmingFailed extends TxState { super(); Object.assign(this, fields); } + kind(): TxStateKind { + return 'PlayerConfirmingFailed'; + } +} + +@variant(2) +export class SettleSucceed extends TxState implements ITxStateKind { + @field('u64') + settleVersion!: bigint; + @field(option('string')) + signature: string | undefined; + + constructor(fields: any) { + super(); + Object.assign(this, fields); + } + kind(): TxStateKind { + return 'SettleSucceed'; + } } diff --git a/js/sdk-core/src/types.ts b/js/sdk-core/src/types.ts index 7e9a6a30..cbc078e5 100644 --- a/js/sdk-core/src/types.ts +++ b/js/sdk-core/src/types.ts @@ -1,5 +1,58 @@ +import { EntryType, INft, IToken } from "./accounts"; +import { Message } from "./broadcast-frames"; +import { ConnectionState } from "./connection"; +import { GameEvent } from "./events"; +import { GameContextSnapshot } from "./game-context-snapshot"; +import { TxState } from "./tx-state"; + export type Id = number; export type Ciphertext = Uint8Array; export type Secret = Uint8Array; export type Digest = Uint8Array; export type Fields = Pick; + +export type GameInfo = { + gameAddr: string; + title: string; + maxPlayers: number; + minDeposit?: bigint; + maxDeposit?: bigint; + entryType: EntryType, + token: IToken; + tokenAddr: string; + bundleAddr: string; + data: Uint8Array; + dataLen: number; +}; + +export type PlayerProfileWithPfp = { + pfp: INft | undefined, + addr: string, + nick: string, +}; + +export type EventCallbackFunction = ( + context: GameContextSnapshot, + state: Uint8Array, + event: GameEvent | undefined, +) => void; + +export type ErrorKind = + | 'event-state-sha-mismatch' + | 'checkpoint-state-sha-mismatch' + | 'onchain-data-not-found' + | 'attach-failed' + | 'handle-event-error' + | 'init-data-invalid' + +export type MessageCallbackFunction = (message: Message) => void; + +export type TxStateCallbackFunction = (txState: TxState) => void; + +export type ConnectionStateCallbackFunction = (connState: ConnectionState) => void; + +export type ProfileCallbackFunction = (id: bigint | undefined, profile: PlayerProfileWithPfp) => void; + +export type LoadProfileCallbackFunction = (id: bigint, addr: string) => void; + +export type ErrorCallbackFunction = (error: ErrorKind, arg: any) => void; diff --git a/js/sdk-core/src/utils.ts b/js/sdk-core/src/utils.ts index 952e9b0d..109dc30a 100644 --- a/js/sdk-core/src/utils.ts +++ b/js/sdk-core/src/utils.ts @@ -1,3 +1,7 @@ +import rfdc from 'rfdc'; + +export const clone = rfdc(); + export function arrayBufferToBase64(buffer: ArrayBuffer): string { let binary = ''; let bytes = new Uint8Array(buffer); diff --git a/js/sdk-core/src/wallet.ts b/js/sdk-core/src/wallet.ts index fb91f336..90234c9b 100644 --- a/js/sdk-core/src/wallet.ts +++ b/js/sdk-core/src/wallet.ts @@ -3,5 +3,5 @@ import { TransactionResult } from "./transport"; export interface IWallet { isConnected: boolean; walletAddr: string; - sendTransaction(tx: any, conn: any): Promise>; + sendTransaction(tx: any, conn: any, config?: any): Promise>; } diff --git a/js/sdk-core/tests/accounts.spec.ts b/js/sdk-core/tests/accounts.spec.ts deleted file mode 100644 index d7e66e9c..00000000 --- a/js/sdk-core/tests/accounts.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { serialize, deserialize } from '@race-foundation/borsh'; -import { assert } from 'chai'; -import * as sut from '../src/accounts'; - -describe('Test accounts with borsh serialization', () => { - it('ServerAccount', () => { - let x = new sut.ServerAccount({ - addr: 'an addr', - endpoint: 'http://foo.bar', - }); - let data = serialize(x); - assert.deepStrictEqual( - data, - Uint8Array.from([ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, - 97, 114, - ]) - ); - let x0 = deserialize(sut.ServerAccount, data); - assert.deepStrictEqual(x, x0); - }); - - it('PlayerProfile without pfp', () => { - let x = new sut.PlayerProfile({ - addr: 'an addr', - nick: 'Alice', - pfp: undefined, - }); - let data = serialize(x); - let x0 = deserialize(sut.PlayerProfile, data); - assert.deepStrictEqual(x, x0); - }); - - it('PlayerProfile with pfp', () => { - let x = new sut.PlayerProfile({ - addr: 'an addr', - nick: 'Alice', - pfp: 'Awesome PFP', - }); - let data = serialize(x); - assert.deepStrictEqual( - data, - Uint8Array.from([ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 5, 0, 0, 0, 65, 108, 105, 99, 101, 1, 11, 0, 0, 0, 65, 119, 101, - 115, 111, 109, 101, 32, 80, 70, 80, - ]) - ); - let x0 = deserialize(sut.PlayerProfile, data); - assert.deepStrictEqual(x, x0); - }); - - it('RegistrationAccount', () => { - let x = new sut.RegistrationAccount({ - addr: 'an addr', - isPrivate: true, - size: 100, - owner: 'another addr', - games: [ - new sut.GameRegistration({ - title: 'Game A', - addr: 'addr 0', - regTime: BigInt(1000), - bundleAddr: 'bundle 0', - }), - new sut.GameRegistration({ - title: 'Game B', - addr: 'addr 1', - regTime: BigInt(1001), - bundleAddr: 'bundle 1', - }), - ], - }); - let data = serialize(x); - assert.deepStrictEqual( - data, - Uint8Array.from([ - 7, 0, 0, 0, 97, 110, 32, 97, 100, 100, 114, 1, 100, 0, 1, 12, 0, 0, 0, 97, 110, 111, 116, 104, 101, 114, 32, 97, - 100, 100, 114, 2, 0, 0, 0, 6, 0, 0, 0, 71, 97, 109, 101, 32, 65, 6, 0, 0, 0, 97, 100, 100, 114, 32, 48, 232, 3, - 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 98, 117, 110, 100, 108, 101, 32, 48, 6, 0, 0, 0, 71, 97, 109, 101, 32, 66, 6, 0, - 0, 0, 97, 100, 100, 114, 32, 49, 233, 3, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 98, 117, 110, 100, 108, 101, 32, 49, - ]) - ); - let x0 = deserialize(sut.RegistrationAccount, data); - assert.deepStrictEqual(x, x0); - }); - - it('GameBundle', () => { - let x = new sut.GameBundle({ - uri: 'http://foo.bar', - name: 'Awesome Game', - data: Uint8Array.of(1, 2, 3, 4), - }); - let data = serialize(x); - assert.deepStrictEqual( - data, - Uint8Array.from([ - 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 12, 0, 0, 0, 65, 119, 101, 115, - 111, 109, 101, 32, 71, 97, 109, 101, 4, 0, 0, 0, 1, 2, 3, 4, - ]) - ); - let x0 = deserialize(sut.GameBundle, data); - assert.deepStrictEqual(x, x0); - }); - - it('GameAccount', () => { - let x = new sut.GameAccount({ - addr: 'game addr', - title: 'awesome game title', - bundleAddr: 'bundle addr', - tokenAddr: 'token addr', - ownerAddr: 'owner addr', - settleVersion: BigInt(10), - accessVersion: BigInt(20), - players: [ - new sut.PlayerJoin({ - addr: 'player 0', - balance: BigInt(3), - position: 0, - accessVersion: BigInt(19), - verifyKey: 'VERIFY KEY', - }), - new sut.PlayerJoin({ - addr: 'player 1', - balance: BigInt(6), - position: 1, - accessVersion: BigInt(21), - verifyKey: 'VERIFY KEY', - }), - ], - deposits: [ - new sut.PlayerDeposit({ - addr: 'player 0', - amount: BigInt(9), - settleVersion: BigInt(21), - }), - new sut.PlayerDeposit({ - addr: 'player 1', - amount: BigInt(12), - settleVersion: BigInt(21), - }), - ], - servers: [ - new sut.ServerJoin({ - addr: 'server 0', - endpoint: 'http://foo.bar', - accessVersion: BigInt(17), - verifyKey: 'VERIFY KEY', - }), - new sut.ServerJoin({ - addr: 'server 1', - endpoint: 'http://foo.bar', - accessVersion: BigInt(17), - verifyKey: 'VERIFY KEY', - }), - ], - transactorAddr: 'server 0', - votes: [ - new sut.Vote({ - voter: 'server 1', - votee: 'server 0', - voteType: sut.VoteType.ServerVoteTransactorDropOff, - }), - ], - unlockTime: undefined, - maxPlayers: 30, - dataLen: 10, - data: Uint8Array.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), - entryType: new sut.EntryTypeCash({ - minDeposit: BigInt(100), - maxDeposit: BigInt(250), - }), - recipientAddr: 'recipient', - checkpoint: Uint8Array.of(10, 11, 12), - checkpointAccessVersion: BigInt(20), - }); - let data = serialize(x); - - assert.deepStrictEqual( - data, - Uint8Array.from([9, 0, 0, 0, 103, 97, 109, 101, 32, 97, 100, 100, 114, 18, 0, 0, 0, 97, 119, 101, 115, 111, 109, 101, 32, 103, 97, 109, 101, 32, 116, 105, 116, 108, 101, 11, 0, 0, 0, 98, 117, 110, 100, 108, 101, 32, 97, 100, 100, 114, 10, 0, 0, 0, 116, 111, 107, 101, 110, 32, 97, 100, 100, 114, 10, 0, 0, 0, 111, 119, 110, 101, 114, 32, 97, 100, 100, 114, 10, 0, 0, 0, 0, 0, 0, 0, 20, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, 48, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 19, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, 49, 1, 0, 6, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 2, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, 48, 9, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 112, 108, 97, 121, 101, 114, 32, 49, 12, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 48, 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 49, 14, 0, 0, 0, 104, 116, 116, 112, 58, 47, 47, 102, 111, 111, 46, 98, 97, 114, 17, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 86, 69, 82, 73, 70, 89, 32, 75, 69, 89, 1, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 48, 1, 0, 0, 0, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 49, 8, 0, 0, 0, 115, 101, 114, 118, 101, 114, 32, 48, 0, 0, 30, 0, 10, 0, 0, 0, 10, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 100, 0, 0, 0, 0, 0, 0, 0, 250, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 114, 101, 99, 105, 112, 105, 101, 110, 116, 3, 0, 0, 0, 10, 11, 12, 20, 0, 0, 0, 0, 0, 0, 0]) - ); - let x0 = deserialize(sut.GameAccount, data); - assert.deepStrictEqual(x, x0); - }); -}); diff --git a/js/sdk-core/tests/effect.spec.ts b/js/sdk-core/tests/effect.spec.ts deleted file mode 100644 index d1ccd542..00000000 --- a/js/sdk-core/tests/effect.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { assert } from 'chai'; -import { ActionTimeout, Ask, Assign, Effect, Release, Reveal, Settle, SettleAdd, SettleSub, Transfer } from '../src/effect'; -import { serialize } from '@race-foundation/borsh'; -import { ShuffledList } from '../src/random-state'; -import { CustomError, NoEnoughPlayers } from '../src/error'; - -describe('Test effect serialization 2', () => { - let effect = new Effect({ - actionTimeout: undefined, - waitTimeout: undefined, - startGame: false, - stopGame: false, - cancelDispatch: false, - timestamp: 1696586237379n, - currRandomId: 1, - currDecisionId: 1, - playersCount: 2, - serversCount: 1, - asks: [], - assigns: [], - reveals: [], - releases: [], - initRandomStates: [], - revealed: new Map(), - answered: new Map(), - isCheckpoint: false, - checkpoint: undefined, - settles: [], - handlerState: Uint8Array.from([0, 0, 0, 0, 0, 0, 0, 0, 16, 39, 0, 0, 0, 0, 0, 0, 32, 78, 0, 0, 0, 0, 0, 0, 32, 78, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6]), - error: new CustomError({ message: "Failed to find a player for the next button" }), - allowExit: true, - transfers: [] - }); - - console.log(Array.from(serialize(effect)).toString()) -}) - -describe('Test effect serialization', () => { - it('serialization', () => { - let effect = new Effect({ - actionTimeout: new ActionTimeout({ playerAddr: 'alice', timeout: 100n }), - waitTimeout: 200n, - startGame: true, - stopGame: true, - cancelDispatch: true, - timestamp: 300_000n, - currRandomId: 1, - currDecisionId: 1, - playersCount: 4, - serversCount: 4, - asks: [ - new Ask({ - playerAddr: 'bob', - }), - ], - assigns: [ - new Assign({ - randomId: 5, - playerAddr: 'bob', - indexes: [0, 1, 2], - }), - ], - reveals: [ - new Reveal({ - randomId: 6, - indexes: [0, 1, 2], - }), - ], - releases: [ - new Release({ - decisionId: 7, - }), - ], - initRandomStates: [ - new ShuffledList({ - options: ['a', 'b'], - }), - ], - revealed: new Map([[22, new Map([[11, 'B']])]]), - answered: new Map([[33, 'A']]), - isCheckpoint: false, - checkpoint: undefined, - settles: [ - new Settle({ - addr: 'alice', - op: new SettleAdd({ amount: 200n }), - }), - new Settle({ - addr: 'bob', - op: new SettleSub({ amount: 200n }), - }), - ], - handlerState: Uint8Array.of(1, 2, 3, 4), - error: new NoEnoughPlayers({}), - allowExit: true, - transfers: [new Transfer({ - slotId: 0, - amount: 100n - })] - }); - const data = serialize(effect); - const expected = Uint8Array.from([ - 1, 5, 0, 0, 0, 97, 108, 105, 99, 101, 100, 0, 0, 0, 0, 0, 0, 0, 1, 200, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 224, 147, 4, - 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 4, 0, 1, 0, 0, 0, 3, 0, 0, 0, 98, 111, 98, 1, - 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 98, 111, 98, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, - 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 97, 1, - 0, 0, 0, 98, 1, 0, 0, 0, 22, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 66, 1, 0, 0, 0, - 33, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 65, 0, 0, 2, 0, 0, 0, 5, 0, 0, 0, 97, 108, 105, 99, 101, 0, 200, 0, 0, 0, 0, 0, 0, - 0, 3, 0, 0, 0, 98, 111, 98, 1, 200, 0, 0, 0, 0, 0, 0, 0, 1, 4, 0, 0, 0, 1, 2, 3, 4, 1, 1, 1, 1, 0, 0, 0, 0, 100, 0, 0, 0, 0, 0, 0, 0, - ]); - assert.deepEqual(data, expected); - }); -}); diff --git a/js/sdk-core/tests/events.spec.ts b/js/sdk-core/tests/events.spec.ts index 9ec773bc..902550f8 100644 --- a/js/sdk-core/tests/events.spec.ts +++ b/js/sdk-core/tests/events.spec.ts @@ -14,7 +14,7 @@ import { Lock, CiphertextAndDigest, RandomnessReady, - Sync, + Join, ServerLeave, Leave, WaitingTimeout, @@ -23,7 +23,7 @@ import { } from '../src/events'; import { assert } from 'chai'; import { deserialize, serialize, field } from '@race-foundation/borsh'; -import { ServerJoin, PlayerJoin } from '../src/accounts'; +import { GamePlayer } from '../src/init-account'; class TestCustom implements ICustomEvent { @field('u32') @@ -41,7 +41,7 @@ class TestCustom implements ICustomEvent { describe('Serialization', () => { it('Custom', () => { - let e = makeCustomEvent('alice', new TestCustom({ n: 100 })); + let e = makeCustomEvent(1n, new TestCustom({ n: 100 })); let data = serialize(e); let e1 = deserialize(GameEvent, data); assert.deepStrictEqual(e, e1); @@ -56,7 +56,7 @@ describe('Serialization', () => { it('ShareSecrets', () => { let e = new ShareSecrets({ - sender: 'alice', + sender: 1n, shares: [ new Random({ fromAddr: 'alice', @@ -78,14 +78,14 @@ describe('Serialization', () => { }); it('OperationTimeout', () => { - let e = new OperationTimeout({ addrs: ['alice', 'bob'] }); + let e = new OperationTimeout({ ids: [1n, 2n] }); let data = serialize(e); let e1 = deserialize(GameEvent, data); assert.deepStrictEqual(e1, e); }); it('Mask', () => { - let e = new Mask({ sender: 'alice', randomId: 1, ciphertexts: [Uint8Array.of(1, 2, 3)] }); + let e = new Mask({ sender: 1n, randomId: 1, ciphertexts: [Uint8Array.of(1, 2, 3)] }); let data = serialize(e); let e1 = deserialize(GameEvent, data); assert.deepStrictEqual(e1, e); @@ -93,7 +93,7 @@ describe('Serialization', () => { it('Lock', () => { let e = new Lock({ - sender: 'alice', + sender: 1n, randomId: 1, ciphertextsAndDigests: [ new CiphertextAndDigest({ @@ -116,27 +116,15 @@ describe('Serialization', () => { assert.deepStrictEqual(e1, e); }); - it('Sync', () => { - let e = new Sync({ - newPlayers: [ - new PlayerJoin({ - addr: 'alice', + it('Join', () => { + let e = new Join({ + players: [ + new GamePlayer({ + id: 1n, position: 1, balance: 100n, - accessVersion: 1n, - verifyKey: 'key0', }), ], - newServers: [ - new ServerJoin({ - addr: 'foo', - endpoint: 'http://foo.bar', - accessVersion: 2n, - verifyKey: 'key1', - }), - ], - transactorAddr: 'foo', - accessVersion: 2n, }); let data = serialize(e); let e1 = deserialize(GameEvent, data); @@ -144,14 +132,14 @@ describe('Serialization', () => { }); it('ServerLeave', () => { - let e = new ServerLeave({ serverAddr: 'foo', transactorAddr: 'bar' }); + let e = new ServerLeave({ serverId: 2n }); let data = serialize(e); let e1 = deserialize(GameEvent, data); assert.deepStrictEqual(e1, e); }); it('Leave', () => { - let e = new Leave({ playerAddr: 'foo' }); + let e = new Leave({ playerId: 1n }); let data = serialize(e); let e1 = deserialize(GameEvent, data); assert.deepStrictEqual(e1, e); @@ -173,7 +161,7 @@ describe('Serialization', () => { it('DrawRandomItems', () => { let e = new DrawRandomItems({ - sender: 'alice', + sender: 1n, randomId: 1, indexes: [10, 20], }); @@ -199,10 +187,10 @@ describe('Serialization', () => { describe('Create custom event', () => { it('Create', () => { - let e = makeCustomEvent('addr', new TestCustom({ n: 1 })); + let e = makeCustomEvent(1n, new TestCustom({ n: 1 })); let e1 = new Custom({ - sender: 'addr', + sender: 1n, raw: Uint8Array.of(1, 0, 0, 0), }); diff --git a/js/sdk-core/tsfmt.json b/js/sdk-core/tsfmt.json index ac46672c..5823aaa0 120000 --- a/js/sdk-core/tsfmt.json +++ b/js/sdk-core/tsfmt.json @@ -1 +1 @@ -/home/tianshu/workspace/race/js/config/tsfmt.json \ No newline at end of file +../config/tsfmt.json \ No newline at end of file diff --git a/js/sdk-facade/src/facade-transport.ts b/js/sdk-facade/src/facade-transport.ts index 936c5821..9fc28dab 100644 --- a/js/sdk-facade/src/facade-transport.ts +++ b/js/sdk-facade/src/facade-transport.ts @@ -24,6 +24,7 @@ import { VoteParams, IStorage, TransactionResult, + ITokenWithBalance, } from '@race-foundation/sdk-core'; import { deserialize } from '@race-foundation/borsh'; @@ -151,8 +152,22 @@ export class FacadeTransport implements ITransport { throw new Error('Method not implemented.'); } - async listTokens(_storage?: IStorage): Promise { - return Object.values(tokenMap); + async listTokens(tokenAddrs: string[], _storage?: IStorage): Promise { + return Object.values(tokenMap).filter(t => tokenAddrs.includes(t.addr)); + } + + async listTokensWithBalance(walletAddr: string, tokenAddrs: string[], storage?: IStorage): Promise { + const balances = await this.fetchBalances(walletAddr, tokenAddrs); + const tokens = Object.values(tokenMap).filter(t => tokenAddrs.includes(t.addr)) + let ret: ITokenWithBalance[] = []; + for (const token of tokens) { + const amount = balances.get(token.addr) || 0n; + const uiAmount = (Number(amount) / Math.pow(10, token.decimals)).toString() + ret.push({ + amount, uiAmount, ...token + }) + } + return ret; } async createPlayerProfile(wallet: IWallet, params: CreatePlayerProfileParams): Promise> { @@ -163,8 +178,12 @@ export class FacadeTransport implements ITransport { } async join(wallet: IWallet, params: JoinParams): Promise> { const playerAddr = wallet.walletAddr; - const ix: JoinInstruction = { playerAddr, ...params }; - if (params.createProfile) { + const gameAccount = await this.getGameAccount(params.gameAddr); + if (gameAccount === undefined) { + throw new Error('Game not found') + } + const ix: JoinInstruction = { playerAddr, accessVersion: gameAccount.accessVersion, ...params }; + if (params.createProfileIfNeeded) { await this.createPlayerProfile(wallet, { nick: wallet.walletAddr.substring(0, 6) }); } await this.sendInstruction('join', ix); @@ -279,6 +298,7 @@ export class FacadeTransport implements ITransport { throw new Error('Failed to fetch data at :' + params); } const { result } = await resp.json(); + console.debug('Facade request:', { method, params, result }); if (result !== null) { return Uint8Array.from(result); } else { diff --git a/js/sdk-facade/tsfmt.json b/js/sdk-facade/tsfmt.json index ac46672c..5823aaa0 120000 --- a/js/sdk-facade/tsfmt.json +++ b/js/sdk-facade/tsfmt.json @@ -1 +1 @@ -/home/tianshu/workspace/race/js/config/tsfmt.json \ No newline at end of file +../config/tsfmt.json \ No newline at end of file diff --git a/js/sdk-solana/src/accounts.ts b/js/sdk-solana/src/accounts.ts index 743eb141..349ea802 100644 --- a/js/sdk-solana/src/accounts.ts +++ b/js/sdk-solana/src/accounts.ts @@ -68,7 +68,6 @@ export interface IGameState { entryType: EntryType; recipientAddr: PublicKey; checkpoint: Uint8Array; - checkpointAccessVersion: bigint; } export interface IServerState { @@ -79,9 +78,9 @@ export interface IServerState { } export interface IRecipientState { - readonly addr: PublicKey; - readonly capAddr: PublicKey | undefined; - readonly slots: IRecipientSlot[]; + isInitialized: boolean; + capAddr: PublicKey | undefined; + slots: IRecipientSlot[]; } const RECIPIENT_SLOT_TYPE = { @@ -103,7 +102,6 @@ export interface IRecipientSlotShare { readonly owner: RecipientSlotOwner; readonly weights: number; readonly claimAmount: bigint; - readonly claimAmountCap: bigint; } export class PlayerState implements IPlayerState { @@ -243,8 +241,6 @@ export class GameState implements IGameState { recipientAddr!: PublicKey; @field('u8-array') checkpoint!: Uint8Array; - @field('u64') - checkpointAccessVersion!: bigint; constructor(fields: IGameState) { Object.assign(this, fields); @@ -259,6 +255,12 @@ export class GameState implements IGameState { } generalize(addr: PublicKey): RaceCore.GameAccount { + + let checkpointOnChain = undefined; + if (this.checkpoint.length !== 0) { + checkpointOnChain = RaceCore.CheckpointOnChain.fromRaw(this.checkpoint); + } + return new RaceCore.GameAccount({ addr: addr.toBase58(), title: this.title, @@ -278,8 +280,7 @@ export class GameState implements IGameState { unlockTime: this.unlockTime, entryType: this.entryType, recipientAddr: this.recipientAddr.toBase58(), - checkpoint: this.checkpoint, - checkpointAccessVersion: this.checkpointAccessVersion, + checkpointOnChain, }); } } @@ -399,8 +400,6 @@ export class RecipientSlotShare implements IRecipientSlotShare { weights!: number; @field('u64') claimAmount!: bigint; - @field('u64') - claimAmountCap!: bigint; constructor(fields: IRecipientSlotShare) { Object.assign(this, fields); } @@ -415,7 +414,7 @@ export class RecipientSlotShare implements IRecipientSlotShare { throw new Error('Invalid slot owner'); } return new RaceCore.RecipientSlotShare({ - owner, weights: this.weights, claimAmount: this.claimAmount, claimAmountCap: this.claimAmountCap + owner, weights: this.weights, claimAmount: this.claimAmount }) } } @@ -435,19 +434,20 @@ export class RecipientSlot implements IRecipientSlot { Object.assign(this, fields); } - generalize(): RaceCore.RecipientSlot { + generalize(balance: bigint): RaceCore.RecipientSlot { return new RaceCore.RecipientSlot({ id: this.id, slotType: this.slotType, tokenAddr: this.tokenAddr.toBase58(), shares: this.shares.map(s => s.generalize()), + balance, }); } } export class RecipientState implements IRecipientState { - @field(publicKeyExt) - addr!: PublicKey; + @field('bool') + isInitialized!: boolean; @field(option(publicKeyExt)) capAddr: PublicKey | undefined; @field(array(struct(RecipientSlot))) @@ -465,11 +465,11 @@ export class RecipientState implements IRecipientState { return deserialize(this, data); } - generalize(): RaceCore.RecipientAccount { + generalize(addr: string, slots: RaceCore.RecipientSlot[]): RaceCore.RecipientAccount { return new RaceCore.RecipientAccount({ - addr: this.addr.toBase58(), + addr, capAddr: this.capAddr?.toBase58(), - slots: this.slots.map(s => s.generalize()), + slots }); } } diff --git a/js/sdk-solana/src/instruction.ts b/js/sdk-solana/src/instruction.ts index 2cc603df..ccb2390e 100644 --- a/js/sdk-solana/src/instruction.ts +++ b/js/sdk-solana/src/instruction.ts @@ -1,10 +1,11 @@ -import { PublicKey, SYSVAR_RENT_PUBKEY, TransactionInstruction } from '@solana/web3.js'; +import { PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram, TransactionInstruction } from '@solana/web3.js'; import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from '@solana/spl-token'; import { publicKeyExt } from './utils'; import { PROGRAM_ID, METAPLEX_PROGRAM_ID, PLAYER_PROFILE_SEED } from './constants'; import { enums, field, serialize } from '@race-foundation/borsh'; import { Buffer } from 'buffer'; import { EntryType } from '@race-foundation/sdk-core'; +import { RecipientSlotOwnerAssigned, RecipientState } from './accounts'; // Instruction types @@ -21,6 +22,9 @@ export enum Instruction { UnregisterGame = 9, JoinGame = 10, PublishGame = 11, + CreateRecipient = 12, + AssignRecipient = 13, + RecipientClaim = 14, } // Instruction data definitations @@ -146,15 +150,55 @@ export type CreateGameOptions = { ownerKey: PublicKey; gameAccountKey: PublicKey; stakeAccountKey: PublicKey; + recipientAccountKey: PublicKey; mint: PublicKey; gameBundleKey: PublicKey; title: string; maxPlayers: number; entryType: EntryType; + data: Uint8Array, }; +export type RegisterGameOptions = { + ownerKey: PublicKey; + gameAccountKey: PublicKey; + registrationAccountKey: PublicKey; +} + +export function registerGame(opts: RegisterGameOptions): TransactionInstruction { + const data = Buffer.from(Uint8Array.of(Instruction.RegisterGame)); + return new TransactionInstruction({ + keys: [ + { + pubkey: opts.ownerKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: opts.registrationAccountKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: opts.gameAccountKey, + isSigner: false, + isWritable: false, + } + ], + programId: PROGRAM_ID, + data, + }); +} + export function createGameAccount(opts: CreateGameOptions): TransactionInstruction { - const data = new CreateGameAccountData(opts).serialize(); + const params = new CreateGameAccountData({ + title: opts.title, + entryType: opts.entryType, + maxPlayers: opts.maxPlayers, + data: opts.data, + }) + console.log('CreateGameAccountParams:', params); + const data = params.serialize(); return new TransactionInstruction({ keys: [ { @@ -187,6 +231,11 @@ export function createGameAccount(opts: CreateGameOptions): TransactionInstructi isSigner: false, isWritable: false, }, + { + pubkey: opts.recipientAccountKey, + isSigner: false, + isWritable: false, + }, ], programId: PROGRAM_ID, data, @@ -244,6 +293,7 @@ export function closeGameAccount(opts: CloseGameAccountOptions): TransactionInst export type JoinOptions = { playerKey: PublicKey; + profileKey: PublicKey; paymentKey: PublicKey; gameAccountKey: PublicKey; mint: PublicKey; @@ -254,12 +304,10 @@ export type JoinOptions = { verifyKey: string; }; -export async function join(opts: JoinOptions): Promise { - const { playerKey, paymentKey, gameAccountKey, mint, stakeAccountKey, amount, accessVersion, position, verifyKey } = +export function join(opts: JoinOptions): TransactionInstruction { + const { playerKey, profileKey, paymentKey, gameAccountKey, mint, stakeAccountKey, amount, accessVersion, position, verifyKey } = opts; - const profileKey = await PublicKey.createWithSeed(playerKey, PLAYER_PROFILE_SEED, PROGRAM_ID); - let [pda, _] = PublicKey.findProgramAddressSync([gameAccountKey.toBuffer()], PROGRAM_ID); const data = new JoinGameData(amount, accessVersion, position, verifyKey).serialize(); @@ -388,3 +436,70 @@ export function publishGame(opts: PublishGameOptions): TransactionInstruction { data, }); } + + +export type ClaimOpts = { + payerKey: PublicKey, + recipientKey: PublicKey, + recipientState: RecipientState, +}; + +export function claim(opts: ClaimOpts): TransactionInstruction { + const [pda, _] = PublicKey.findProgramAddressSync([opts.recipientKey.toBuffer()], PROGRAM_ID); + + let keys = [ + { + pubkey: opts.payerKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: opts.recipientKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: pda, + isSigner: false, + isWritable: false, + }, + { + pubkey: TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + for (const slot of opts.recipientState.slots) { + for (const slotShare of slot.shares) { + if (slotShare.owner instanceof RecipientSlotOwnerAssigned && slotShare.owner.addr === opts.payerKey) { + keys.push({ + pubkey: slot.stakeAddr, + isSigner: false, + isWritable: false, + }); + const ata = getAssociatedTokenAddressSync(slotShare.owner.addr, slot.tokenAddr); + keys.push({ + pubkey: ata, + isSigner: false, + isWritable: false, + }) + } + } + } + + if (keys.length === 5) { + throw new Error('No slot to claim'); + } + + return new TransactionInstruction({ + keys, + programId: PROGRAM_ID, + data: Buffer.from(Uint8Array.of(Instruction.RecipientClaim)), + }); +} diff --git a/js/sdk-solana/src/solana-transport.ts b/js/sdk-solana/src/solana-transport.ts index 53c594f3..0a6607c3 100644 --- a/js/sdk-solana/src/solana-transport.ts +++ b/js/sdk-solana/src/solana-transport.ts @@ -1,7 +1,8 @@ -import { SystemProgram, Connection, PublicKey, Transaction, Keypair } from '@solana/web3.js'; +import { SystemProgram, Connection, PublicKey, Keypair, ComputeBudgetProgram, TransactionMessage, TransactionInstruction, VersionedTransaction, AccountInfo } from '@solana/web3.js'; import { Buffer } from 'buffer'; import { AccountLayout, + MintLayout, NATIVE_MINT, TOKEN_PROGRAM_ID, createInitializeAccountInstruction, @@ -31,12 +32,16 @@ import { INft, RegistrationWithGames, RecipientAccount, + RecipientSlot, RecipientClaimParams, EntryTypeCash, IStorage, getTtlCache, setTtlCache, TransactionResult, + ITokenWithBalance, + TokenWithBalance, + Token, } from '@race-foundation/sdk-core'; import * as instruction from './instruction'; @@ -48,7 +53,6 @@ import { join } from './instruction'; import { PROGRAM_ID, METAPLEX_PROGRAM_ID } from './constants'; import { Metadata } from './metadata'; -const TOKEN_CACHE_TTL = 24 * 3600; const NFT_CACHE_TTL = 24 * 30 * 3600; function trimString(s: string): string { @@ -82,7 +86,7 @@ export class SolanaTransport implements ITransport { } async createGameAccount(wallet: IWallet, params: CreateGameAccountParams): Promise> { - const { title, bundleAddr, tokenAddr } = params; + const { title, bundleAddr, tokenAddr, recipientAddr } = params; if (title.length > NAME_LEN) { // FIXME: better error message? throw Error('Game title length exceeds 16 chars'); @@ -92,10 +96,11 @@ export class SolanaTransport implements ITransport { const payerKey = new PublicKey(wallet.walletAddr); console.log('Payer publick key: ', payerKey); - let tx = await makeTransaction(conn, payerKey); + let ixs = []; const gameAccount = Keypair.generate(); const gameAccountKey = gameAccount.publicKey; + const registrationAccountKey = new PublicKey(params.registrationAddr); const lamports = await conn.getMinimumBalanceForRentExemption(GAME_ACCOUNT_LEN); const createGameAccount = SystemProgram.createAccount({ fromPubkey: payerKey, @@ -104,8 +109,10 @@ export class SolanaTransport implements ITransport { space: GAME_ACCOUNT_LEN, programId: PROGRAM_ID, }); - tx.add(createGameAccount); + console.log(createGameAccount); + ixs.push(createGameAccount); + const recipientAccountKey = new PublicKey(recipientAddr); const tokenMintKey = new PublicKey(tokenAddr); const stakeAccount = Keypair.generate(); const stakeAccountKey = stakeAccount.publicKey; @@ -117,7 +124,8 @@ export class SolanaTransport implements ITransport { space: AccountLayout.span, programId: TOKEN_PROGRAM_ID, }); - tx.add(createStakeAccount); + console.log(createStakeAccount); + ixs.push(createStakeAccount); const initStakeAccount = createInitializeAccountInstruction( stakeAccountKey, @@ -125,23 +133,39 @@ export class SolanaTransport implements ITransport { payerKey, TOKEN_PROGRAM_ID ); - tx.add(initStakeAccount); + console.log(initStakeAccount); + ixs.push(initStakeAccount); const bundleKey = new PublicKey(bundleAddr); const createGame = instruction.createGameAccount({ ownerKey: payerKey, gameAccountKey: gameAccountKey, stakeAccountKey: stakeAccountKey, + recipientAccountKey: recipientAccountKey, mint: tokenMintKey, gameBundleKey: bundleKey, title: title, maxPlayers: params.maxPlayers, entryType: params.entryType, + data: params.data, }); - tx.add(createGame); - tx.partialSign(gameAccount, stakeAccount); + console.log(createGame); + ixs.push(createGame); + + const registerGame = instruction.registerGame({ + ownerKey: payerKey, + gameAccountKey: gameAccountKey, + registrationAccountKey: registrationAccountKey + }); + console.log(registerGame); + ixs.push(registerGame); + + let tx = await makeTransaction(this.#conn, payerKey, ixs); + + const res = await wallet.sendTransaction(tx, conn, + { signers: [gameAccount, stakeAccount] } + ); - const res = await wallet.sendTransaction(tx, conn); if (res.result === 'ok') { return { result: 'ok', value: gameAccountKey.toBase58() }; } else { @@ -154,17 +178,30 @@ export class SolanaTransport implements ITransport { } async join(wallet: IWallet, params: JoinParams): Promise> { - const conn = this.#conn; - const { gameAddr, amount: amountRaw, accessVersion: accessVersionRaw, position, verifyKey } = params; + let ixs = []; - const accessVersion = BigInt(accessVersionRaw); - const playerKey = new PublicKey(wallet.walletAddr); + const tempAccountLen = AccountLayout.span; + + const conn = this.#conn; + const { gameAddr, amount: amountRaw, position, verifyKey } = params; const gameAccountKey = new PublicKey(gameAddr); - const gameState = await this._getGameState(gameAccountKey); + const playerKey = new PublicKey(wallet.walletAddr); + + // Call RPC functions in Parallel + const [tempAccountLamports, prioritizationFee, gameState, playerProfile] = await Promise.all([ + conn.getMinimumBalanceForRentExemption(tempAccountLen), + this._getPrioritizationFee([gameAccountKey]), + this._getGameState(gameAccountKey), + this.getPlayerProfile(wallet.walletAddr) + ]) + + const profileKey0 = playerProfile !== undefined ? new PublicKey(playerProfile?.addr) : undefined; + if (gameState === undefined) { throw new Error('Game account not found'); } + const accessVersion = gameState.accessVersion; if (!(gameState.entryType instanceof EntryTypeCash)) { throw new Error('Unsupported entry type'); } @@ -185,15 +222,18 @@ export class SolanaTransport implements ITransport { const stakeAccountKey = gameState.stakeKey; const tempAccountKeypair = Keypair.generate(); const tempAccountKey = tempAccountKeypair.publicKey; - const tempAccountLen = AccountLayout.span; - const tempAccountLamports = await conn.getMinimumBalanceForRentExemption(tempAccountLen); - const tx = await makeTransaction(conn, playerKey); + ixs.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: prioritizationFee })) - if (params.createProfile) { - await this.addCreateProfileIxToTransaction(tx, wallet, { + let profileKey: PublicKey; + if (profileKey0 !== undefined) { + profileKey = profileKey0; + } else if (params.createProfileIfNeeded) { + profileKey = await this.appendCreateProfileIxs(ixs, wallet, { nick: wallet.walletAddr.substring(0, 6), - }); + }) + } else { + throw new Error('Player has no profile account'); } const createTempAccountIx = SystemProgram.createAccount({ @@ -203,10 +243,10 @@ export class SolanaTransport implements ITransport { space: tempAccountLen, programId: TOKEN_PROGRAM_ID, }); - tx.add(createTempAccountIx); + ixs.push(createTempAccountIx); const initTempAccountIx = createInitializeAccountInstruction(tempAccountKey, mintKey, playerKey); - tx.add(initTempAccountIx); + ixs.push(initTempAccountIx); if (isWsol) { const transferAmount = amount - BigInt(tempAccountLamports); @@ -215,15 +255,16 @@ export class SolanaTransport implements ITransport { toPubkey: tempAccountKey, lamports: transferAmount, }); - tx.add(transferIx); + ixs.push(transferIx); } else { const playerAta = getAssociatedTokenAddressSync(mintKey, playerKey); const transferIx = createTransferInstruction(playerAta, tempAccountKey, playerKey, amount); - tx.add(transferIx); + ixs.push(transferIx); } - const joinGameIx = await join({ + const joinGameIx = join({ playerKey, + profileKey, paymentKey: tempAccountKey, gameAccountKey, mint: mintKey, @@ -233,11 +274,10 @@ export class SolanaTransport implements ITransport { position, verifyKey, }); - tx.add(joinGameIx); - - tx.partialSign(tempAccountKeypair); + ixs.push(joinGameIx); - return await wallet.sendTransaction(tx, this.#conn); + const tx = await makeTransaction(this.#conn, playerKey, ixs); + return await wallet.sendTransaction(tx, this.#conn, { signers: [tempAccountKeypair] }); } async deposit(_wallet: IWallet, _params: DepositParams): Promise> { @@ -252,20 +292,33 @@ export class SolanaTransport implements ITransport { throw new Error('unimplemented'); } - async recipientClaim(_wallet: IWallet, _params: RecipientClaimParams): Promise> { - throw new Error('unimplemented'); + async recipientClaim(wallet: IWallet, params: RecipientClaimParams): Promise> { + const payerKey = new PublicKey(wallet.walletAddr); + const recipientKey = new PublicKey(params.recipientAddr); + const recipientState = await this._getRecipientState(recipientKey); + + if (recipientState === undefined) { + throw new Error('Recipient account not found'); + } + + const recipientClaimIx = instruction.claim({ + recipientKey, payerKey, recipientState + }); + const tx = await makeTransaction(this.#conn, payerKey, [recipientClaimIx]); + + return await wallet.sendTransaction(tx, this.#conn); } - async addCreateProfileIxToTransaction(tx: Transaction, wallet: IWallet, params: CreatePlayerProfileParams): Promise { + async appendCreateProfileIxs(ixs: TransactionInstruction[], wallet: IWallet, params: CreatePlayerProfileParams): Promise { const { nick, pfp } = params; if (nick.length > 16) { throw new Error('Player nick name exceeds 16 chars'); } const payerKey = new PublicKey(wallet.walletAddr); - console.log('Payer Public Key:', payerKey); + console.log('Payer Public Key:', payerKey.toBase58()); const profileKey = await PublicKey.createWithSeed(payerKey, PLAYER_PROFILE_SEED, PROGRAM_ID); - console.log('Player profile public key: ', profileKey); + console.log('Player profile public key: ', profileKey.toBase58()); if (!(await this.#conn.getAccountInfo(profileKey))) { let lamports = await this.#conn.getMinimumBalanceForRentExemption(PROFILE_ACCOUNT_LEN); @@ -279,31 +332,37 @@ export class SolanaTransport implements ITransport { space: PROFILE_ACCOUNT_LEN, programId: PROGRAM_ID, }); - tx.add(createProfileAccount); + ixs.push(createProfileAccount); } const pfpKey = !pfp ? PublicKey.default : new PublicKey(pfp); const createProfile = instruction.createPlayerProfile(payerKey, profileKey, nick, pfpKey); - tx.add(createProfile); + ixs.push(createProfile); + return profileKey; } async createPlayerProfile(wallet: IWallet, params: CreatePlayerProfileParams): Promise> { + let ixs: TransactionInstruction[] = []; + const payerKey = new PublicKey(wallet.walletAddr); - let tx = await makeTransaction(this.#conn, payerKey); - await this.addCreateProfileIxToTransaction(tx, wallet, params); + // const prioritizationFee = await this._getPrioritizationFee([]); + // ixs.push(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: prioritizationFee })); + await this.appendCreateProfileIxs(ixs, wallet, params); + + let tx = await makeTransaction(this.#conn, payerKey, ixs); return await wallet.sendTransaction(tx, this.#conn); } - async createRegistration(wallet: IWallet, params: CreateRegistrationParams): Promise> { + async createRegistration(_wallet: IWallet, _params: CreateRegistrationParams): Promise> { throw new Error('unimplemented'); } - async registerGame(wallet: IWallet, params: RegisterGameParams): Promise> { + async registerGame(_wallet: IWallet, _params: RegisterGameParams): Promise> { throw new Error('unimplemented'); } - async unregisterGame(wallet: IWallet, params: UnregisterGameParams): Promise> { + async unregisterGame(_wallet: IWallet, _params: UnregisterGameParams): Promise> { throw new Error('unimplemented'); } @@ -397,14 +456,17 @@ export class SolanaTransport implements ITransport { }); } - async getRecipient(addr: String): Promise { + async getRecipient(addr: string): Promise { const recipientKey = new PublicKey(addr); const recipientState = await this._getRecipientState(recipientKey); - if (recipientState !== undefined) { - return recipientState.generalize(); - } else { - return undefined; + if (recipientState === undefined) return undefined; + let slots: RecipientSlot[] = []; + for (const slot of recipientState.slots) { + const resp = await this.#conn.getTokenAccountBalance(slot.stakeAddr); + const balance = BigInt(resp.value.amount); + slots.push(slot.generalize(balance)); } + return recipientState.generalize(addr, slots); } async _fetchImageFromDataUri(dataUri: string): Promise { @@ -417,6 +479,18 @@ export class SolanaTransport implements ITransport { } } + async _getPrioritizationFee(pubkeys: PublicKey[]): Promise { + const prioritizationFee = await this.#conn.getRecentPrioritizationFees({ + lockedWritableAccounts: pubkeys + }); + let f = 0; + for (const fee of prioritizationFee) { + f = fee.prioritizationFee; + } + console.log('Prioritization fee:', f); + return f; + } + async getToken(addr: string): Promise { const mintKey = new PublicKey(addr); try { @@ -465,58 +539,147 @@ export class SolanaTransport implements ITransport { } } - /** - * List popular tokens. - * - * [USDT, USDC, SOL, RACE] - */ - async listTokens(storage?: IStorage): Promise { - const popularTokenAddrs = [ - 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', - 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - 'So11111111111111111111111111111111111111112', - ]; - - let tokens = []; - for (const addr of popularTokenAddrs) { - const cacheKey = `TOKEN_CACHE__${this.chain}__${addr}`; - - // Read token info from cache - if (storage !== undefined) { - const tokenInfo: IToken | undefined = getTtlCache(storage, cacheKey) - if (tokenInfo !== undefined) { - tokens.push(tokenInfo); - continue; - } + // Return [name, symbol, icon] + async parseTokenMetadata(addr: string, metadataAccount: AccountInfo): Promise<[string | undefined, string | undefined, string | undefined]> { + const metadataState = Metadata.deserialize(metadataAccount.data); + const uri = trimString(metadataState.data.uri); + const image = uri ? await this._fetchImageFromDataUri(uri): undefined; + if (this.#legacyTokens === undefined) { + await this._fetchLegacyTokens(); + } + let legacyToken: LegacyToken | undefined = undefined; + if (this.#legacyTokens !== undefined) { + legacyToken = this.#legacyTokens.find(t => t.address === addr); + } + + const name = metadataState.data.name + ? trimString(metadataState.data.name) + : legacyToken + ? legacyToken.name + : ''; + const symbol = metadataState.data.symbol + ? trimString(metadataState.data.symbol) + : legacyToken + ? legacyToken.symbol + : ''; + const icon = image ? image : legacyToken?.logoURI ? legacyToken.logoURI : ''; + return [name, symbol, icon]; + } + + async listTokens(tokenAddrs: string[], storage?: IStorage): Promise { + if (tokenAddrs.length > 30) { + throw new Error('Too many token addresses in a row'); + } + + let results = []; + + let mintMetaList: [PublicKey, PublicKey][] = []; + for (const t of tokenAddrs) { + const mintKey = new PublicKey(t); + const [metadataKey] = PublicKey.findProgramAddressSync( + [Buffer.from('metadata', 'utf8'), METAPLEX_PROGRAM_ID.toBuffer(), mintKey.toBuffer()], + METAPLEX_PROGRAM_ID + ); + mintMetaList.push([mintKey, metadataKey]); + } + + const accountInfos = await this.#conn.getMultipleAccountsInfo(mintMetaList.flat()) + for (let i = 0; i < mintMetaList.length; i++) { + const mintKey = mintMetaList[i][0]; + const mintAccount = accountInfos[2 * i]; + const metadataAccount = accountInfos[2 * i + 1]; + + let addr = mintKey.toBase58(); + let decimals: number | undefined = undefined; + let name: string | undefined = undefined; + let symbol: string | undefined = undefined; + let icon: string | undefined = undefined; + + if (mintAccount) { + const m = MintLayout.decode(mintAccount.data); + decimals = m.decimals; } - // Read on-chain data - const tokenInfo = await this.getToken(addr); - if (tokenInfo !== undefined) { - tokens.push(tokenInfo); + if (metadataAccount) { + [name, symbol, icon] = await this.parseTokenMetadata(addr, metadataAccount); + } - // Save to cache - if (storage !== undefined) { - setTtlCache(storage, cacheKey, tokenInfo, TOKEN_CACHE_TTL); - } + console.log(addr, decimals, name, symbol, icon); + + if (decimals !== undefined + && name !== undefined + && symbol !== undefined + && icon !== undefined + ) { + results.push(new Token({ addr, name, symbol, icon, decimals })); } } - return tokens; + + return results; } - async fetchBalances(walletAddr: string, tokenAddrs: string[]): Promise> { - const walletKey = new PublicKey(walletAddr); - let ret = new Map(); - for (const tokenAddr of tokenAddrs) { - const tokenAccountKey = getAssociatedTokenAddressSync(new PublicKey(tokenAddr), walletKey); - try { - const resp = await this.#conn.getTokenAccountBalance(tokenAccountKey); - ret.set(tokenAddr, BigInt(resp.value.amount)); - } catch (e) { - ret.set(tokenAddr, 0n); + /** + * List tokens. + */ + async listTokensWithBalance(walletAddr: string, tokenAddrs: string[], storage?: IStorage): Promise { + if (tokenAddrs.length > 30) { + throw new Error('Too many token addresses in a row'); + } + + let results = []; + + const ownerKey = new PublicKey(walletAddr); + let mintAtaList: [PublicKey, PublicKey, PublicKey][] = []; + for (const t of tokenAddrs) { + const mintKey = new PublicKey(t); + const ataKey = getAssociatedTokenAddressSync(mintKey, ownerKey); + const [metadataKey] = PublicKey.findProgramAddressSync( + [Buffer.from('metadata', 'utf8'), METAPLEX_PROGRAM_ID.toBuffer(), mintKey.toBuffer()], + METAPLEX_PROGRAM_ID + ); + mintAtaList.push([mintKey, ataKey, metadataKey]); + } + + const accountInfos = await this.#conn.getMultipleAccountsInfo(mintAtaList.flat()) + for (let i = 0; i < mintAtaList.length; i++) { + const mintKey = mintAtaList[i][0]; + const mintAccount = accountInfos[3 * i]; + const ataAccount = accountInfos[3 * i + 1]; + const metadataAccount = accountInfos[3 * i + 2]; + + let addr = mintKey.toBase58(); + let decimals: number | undefined = undefined; + let name: string | undefined = undefined; + let symbol: string | undefined = undefined; + let icon: string | undefined = undefined; + let balance: bigint = 0n; + + if (mintAccount) { + const m = MintLayout.decode(mintAccount.data); + decimals = m.decimals; + } + + if (metadataAccount) { + [name, symbol, icon] = await this.parseTokenMetadata(addr, metadataAccount); + } + + if (ataAccount) { + const acc = AccountLayout.decode(ataAccount.data); + balance = acc.amount; + } + + console.log(addr, decimals, name, symbol, icon, balance); + + if (decimals !== undefined + && name !== undefined + && symbol !== undefined + && icon !== undefined + ) { + results.push(new TokenWithBalance({ addr, name, symbol, icon, decimals }, balance)); } } - return ret; + + return results; } async getNft(addr: string | PublicKey, storage?: IStorage): Promise { @@ -615,17 +778,6 @@ export class SolanaTransport implements ITransport { } } - async _getRecipientState(recipientAccountKey: PublicKey): Promise { - const conn = this.#conn; - const recipientAccount = await conn.getAccountInfo(recipientAccountKey); - if (recipientAccount !== null) { - const data = recipientAccount.data; - return RecipientState.deserialize(data); - } else { - return undefined; - } - } - async _getMultiGameStates(gameAccountKeys: PublicKey[]): Promise> { const conn = this.#conn; const accountsInfo = await conn.getMultipleAccountsInfo(gameAccountKeys); @@ -650,6 +802,17 @@ export class SolanaTransport implements ITransport { return ret; } + async _getRecipientState(recipientKey: PublicKey): Promise { + const conn = this.#conn; + const recipientAccount = await conn.getAccountInfo(recipientKey); + if (recipientAccount !== null) { + const data = recipientAccount.data; + return RecipientState.deserialize(data); + } else { + return undefined; + } + } + async _getRegState(regKey: PublicKey): Promise { const conn = this.#conn; const regAccount = await conn.getAccountInfo(regKey); @@ -674,11 +837,24 @@ export class SolanaTransport implements ITransport { } } -async function makeTransaction(conn: Connection, feePayer: PublicKey): Promise { - const { blockhash, lastValidBlockHeight } = await conn.getLatestBlockhash(); - return new Transaction({ - feePayer, - blockhash, - lastValidBlockHeight, - }); +async function makeTransaction( + conn: Connection, + feePayer: PublicKey, + instructions: TransactionInstruction[] +): Promise { + const slot = await conn.getSlot(); + const block = await conn.getBlock(slot, { maxSupportedTransactionVersion: 0, transactionDetails: 'none' }); + if (block === null) { + throw new Error('Cannot find block'); + } + + const messageV0 = new TransactionMessage({ + payerKey: feePayer, + recentBlockhash: block.blockhash, + instructions + }).compileToV0Message(); + + const transaction = new VersionedTransaction(messageV0); + + return transaction; } diff --git a/js/sdk-solana/src/solana-wallet.ts b/js/sdk-solana/src/solana-wallet.ts index 70ee4cba..aedb0e14 100644 --- a/js/sdk-solana/src/solana-wallet.ts +++ b/js/sdk-solana/src/solana-wallet.ts @@ -16,12 +16,12 @@ export class SolanaWalletAdapter implements IWallet { this.#wallet = wallet; } - async sendTransaction(tx: TransactionInstruction, conn: Connection): Promise> { + async sendTransaction(tx: TransactionInstruction, conn: Connection, config?: any): Promise> { const { - context: { slot: minContextSlot }, value: { blockhash, lastValidBlockHeight }, } = await conn.getLatestBlockhashAndContext(); - const signature: TransactionSignature = await this.#wallet.sendTransaction(tx, conn, { minContextSlot }); + + const signature: TransactionSignature = await this.#wallet.sendTransaction(tx, conn, config); const resp = await conn.confirmTransaction({ blockhash, lastValidBlockHeight, signature }); if (resp.value.err !== null) { return { diff --git a/js/sdk-solana/tests/accounts.spec.ts b/js/sdk-solana/tests/accounts.spec.ts index 948874cf..38869339 100644 --- a/js/sdk-solana/tests/accounts.spec.ts +++ b/js/sdk-solana/tests/accounts.spec.ts @@ -10,8 +10,8 @@ import { Vote, } from '../src/accounts'; import { PublicKey } from '@solana/web3.js'; -import { ACCOUNT_DATA, REG_ACCOUNT_DATA } from './account_data'; -import { EntryType, EntryTypeCash } from '@race-foundation/sdk-core'; +import { REG_ACCOUNT_DATA } from './account_data'; +import { EntryTypeCash } from '@race-foundation/sdk-core'; describe('Test account data serialization', () => { it('PlayerState', () => { @@ -127,7 +127,6 @@ describe('Test account data serialization', () => { }), recipientAddr: PublicKey.unique(), checkpoint: Uint8Array.of(1, 2, 3, 4), - checkpointAccessVersion: BigInt(2), }); let buf = state.serialize(); let deserialized = GameState.deserialize(buf); diff --git a/js/sdk-solana/tsfmt.json b/js/sdk-solana/tsfmt.json index ac46672c..5823aaa0 120000 --- a/js/sdk-solana/tsfmt.json +++ b/js/sdk-solana/tsfmt.json @@ -1 +1 @@ -/home/tianshu/workspace/race/js/config/tsfmt.json \ No newline at end of file +../config/tsfmt.json \ No newline at end of file diff --git a/legacy/sdk/Cargo.toml b/legacy/sdk/Cargo.toml deleted file mode 100644 index 5c7cb085..00000000 --- a/legacy/sdk/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "race-sdk" -version = "0.1.0" -edition = "2021" - -[lib] -# crate-type = ["cdylib", "rlib"] -crate-type = ["cdylib"] - -[dependencies] -race-core = { path = "../core" } -race-client = { path = "../client" } -race-encryptor = { path = "../encryptor" } -race-transport = { path = "../transport" } -web-sys = { workspace = true, features = ["Worker", "MessageEvent"] } -jsonrpsee = { workspace = true, features = ["wasm-client"] } -getrandom = { workspace = true, features = ["js"] } -gloo = { workspace = true } -anyhow = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -borsh = { workspace = true } -thiserror = { workspace = true } -futures = { workspace = true } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -js-sys = { workspace = true } -async-stream = { workspace = true } -async-trait = { workspace = true } -base64 = { workspace = true } - -[target.'cfg(target_arch = "wasm32")'.dev-dependencies] -wasm-bindgen-test = { workspace = true } diff --git a/legacy/sdk/package.json.patch b/legacy/sdk/package.json.patch deleted file mode 100644 index dbc5ce72..00000000 --- a/legacy/sdk/package.json.patch +++ /dev/null @@ -1,9 +0,0 @@ ---- pkg/package.json 2023-02-19 16:24:39.337640017 +0800 -+++ new_package.json 2023-02-19 16:24:18.734250615 +0800 -@@ -1,5 +1,6 @@ - { - "name": "race-sdk", -+ "main": "./race_sdk.js", - "version": "0.1.0", - "files": [ - "race_sdk_bg.wasm", diff --git a/legacy/sdk/src/app_client.rs b/legacy/sdk/src/app_client.rs deleted file mode 100644 index 50845954..00000000 --- a/legacy/sdk/src/app_client.rs +++ /dev/null @@ -1,395 +0,0 @@ -//! A common client to use in dapp(native version). - -use gloo::utils::format::JsValueSerdeExt; -use js_sys::JSON::{parse, stringify}; -use js_sys::{Function, Object, Reflect}; -use jsonrpsee::core::client::Subscription; -use race_core::context::GameContext; -use race_core::prelude::{DecisionId, InitAccount}; -use race_core::types::{BroadcastFrame, ExitGameParams, GameAccount, RandomId}; -use wasm_bindgen::prelude::*; -use race_transport::wasm_trait::TransportLocalT; - -use futures::pin_mut; -use futures::stream::StreamExt; -use std::cell::RefCell; -use std::sync::Arc; - -use crate::connection::Connection; -use crate::handler::Handler; -use crate::transport::Transport; -use crate::utils::rget; -use gloo::console::{debug, error, info, warn}; - -use crate::error::Result; -use crate::js::{Event as JsEvent, JsGameContext}; -use race_client::Client; -use race_core::{ - connection::ConnectionT, - error::Error, - event::Event, - types::{ClientMode, GetStateParams, JoinParams, SubmitEventParams}, -}; -use race_encryptor::Encryptor; - -#[wasm_bindgen] -pub struct AppClient { - addr: String, - client: Client, - handler: Handler, - wallet: JsValue, - transport: Arc, - connection: Arc, - game_context: RefCell, - init_game_account: RefCell>, - event_sub: RefCell>>, - callback: Function, -} - -#[wasm_bindgen] -impl AppClient { - /// Try initialize an app client, which will connect to transactor and blockchain RPC. - /// - /// # Arguments - /// * `chain`, The name of blockchain, currently only `"facade"` is supported. - /// * `rpc`, The endpoint of blockchain RPC. - /// * `player_addr`, The address of current player. - /// * `game_addr`, The address of game to attach. - /// * `callback`, A JS function: function(addr: String, context: PartialGameContext, state: GameState). - /// This function will be called when either game context or game state is updated. - /// The `addr` belong to either the game or its sub game. - #[wasm_bindgen] - pub async fn try_init( - transport: JsValue, - wallet: JsValue, - game_addr: &str, - callback: Function, - ) -> Result { - let transport = Arc::new(Transport::new(transport)); - AppClient::try_new(transport, wallet, game_addr, callback).await - } - - async fn try_new( - transport: Arc, - wallet: JsValue, - game_addr: &str, - callback: Function, - ) -> Result { - let encryptor = Arc::new(Encryptor::default()); - info!("WASM: Encryptor created"); - info!(&wallet); - let player_addr = rget(&wallet, "walletAddr").as_string().unwrap(); - info!("WASM: Player addr got"); - let game_account = transport - .get_game_account(game_addr) - .await? - .ok_or(Error::GameAccountNotFound)?; - info!("WASM: Game account loaded"); - let game_bundle = transport - .get_game_bundle(&game_account.bundle_addr) - .await? - .ok_or(Error::GameBundleNotFound)?; - info!("WASM: Game bundle loaded"); - let transactor_addr = game_account - .transactor_addr - .as_ref() - .ok_or(Error::GameNotServed)?; - info!("WASM: Game is served"); - let transactor_account = transport - .get_server_account(transactor_addr) - .await? - .ok_or(Error::CantFindTransactor)?; - info!("WASM: Transactor account loaded"); - let connection = Arc::new( - Connection::try_new( - &player_addr, - &transactor_account.endpoint, - encryptor.clone(), - ) - .await?, - ); - info!("WASM: Connection initialized"); - let client = Client::new( - player_addr.to_owned(), - game_addr.to_owned(), - ClientMode::Player, - transport.clone(), - encryptor.clone(), - connection.clone(), - ); - info!("WASM: Game client created"); - - let handler = Handler::from_bundle(game_bundle, encryptor).await?; - - let game_context = RefCell::new(GameContext::try_new(&game_account)?); - - Ok(Self { - addr: game_addr.to_owned(), - client, - wallet, - transport, - connection, - handler, - game_context, - init_game_account: RefCell::new(Some(game_account)), - callback, - event_sub: RefCell::new(None), - }) - } - - fn invoke_callback(&self, game_context: &GameContext, event: Option) -> Result<()> { - let state = parse(game_context.get_handler_state_raw()).or(Err(Error::JsonParseError))?; - - let context = JsGameContext::from_context(&game_context); - let event_js: JsValue = if let Some(event) = event { - let event = JsEvent::from(event); - event.into() - } else { - JsValue::UNDEFINED - }; - - let r = Function::call3( - &self.callback, - &JsValue::NULL, - &JsValue::from_serde(&context).or(Err(Error::JsonParseError))?, - &state, - &event_js, - ); - if let Err(e) = r { - error!(format!("WASM: Callback error, {:?}", e)); - } - Ok(()) - } - - #[wasm_bindgen] - pub async fn get_profile(&self, addr: &str) -> Result> { - if let Some(p) = self.transport.get_player_profile(addr).await? { - Ok(Some(JsValue::from_serde(&p).unwrap())) - } else { - Ok(None) - } - } - - #[wasm_bindgen] - /// Attach to game account on chain and connect to the event - /// streams. The event stream will start from a - /// checkpoint(settle_version). We will receive event hhistories - /// once the connection is established. - pub async fn attach_game(&self) -> Result<()> { - let mut init_game_account = self.init_game_account.borrow_mut(); - if init_game_account.is_none() { - return Err(Error::DuplicatedInitialization)?; - } - info!("WASM: Attach to game"); - self.client.attach_game().await?; - let settle_version = self.game_context.borrow().get_settle_version(); - - debug!( - "WASM: Subscribe event stream, use settle_version = {} as check point", - settle_version - ); - - let sub = self - .connection - .subscribe_events(&self.addr, settle_version) - .await?; - - pin_mut!(sub); - debug!("WASM: Event stream connected"); - - while let Some(Ok(frame)) = sub.next().await { - match frame { - BroadcastFrame::Init { - access_version, - settle_version, - .. - } => { - let mut game_context = self.game_context.borrow_mut(); - let game_account = std::mem::replace(&mut *init_game_account, None) - .ok_or(Error::DuplicatedInitialization)?; - - info!(format!( - "WASM: Apply checkpoint, access_version = {}, settle_version = {}", - access_version, settle_version - )); - - game_context.apply_checkpoint(access_version, settle_version)?; - - let init_account = - InitAccount::new(game_account, access_version, settle_version); - match self.handler.init_state(&mut game_context, &init_account) { - Ok(_) => { - self.invoke_callback(&game_context, None)?; - } - Err(Error::WasmExecutionError(e)) => { - error!(format!("WASM: Initiate state error: {:?}", e)) - } - Err(e) => { - warn!("WASM: Init state failed, {}", e.to_string()) - } - } - } - BroadcastFrame::Event { - event, timestamp, .. - } => { - let mut game_context = self.game_context.borrow_mut(); - game_context.set_timestamp(timestamp); - match self.handler.handle_event(&mut game_context, &event) { - Ok(_) => { - self.invoke_callback(&game_context, Some(event))?; - } - Err(Error::WasmExecutionError(e)) => { - error!(format!("WASM: Handle event error: {:?}", e)) - } - Err(e) => { - warn!(format!("WASM: Discard event [{}] due to: [{:?}]", event, e)); - } - } - } - } - } - Ok(()) - } - - #[wasm_bindgen] - pub async fn submit_event(&self, val: JsValue) -> Result<()> { - info!(format!("WASM: Submit event: {:?}", val)); - let raw = stringify(&val) - .or(Err(Error::JsonParseError))? - .as_string() - .ok_or(Error::JsonParseError)?; - let event = Event::Custom { - sender: self.client.addr.clone(), - raw, - }; - self.connection - .submit_event(&self.addr, SubmitEventParams { event }) - .await?; - Ok(()) - } - - #[wasm_bindgen] - /// Get all revealed information. This function contains the - /// decryption, so it's better to cache the result somewhere. - pub fn get_revealed(&self, random_id: RandomId) -> Result { - let context = self.game_context.borrow(); - let decrypted = self.client.decrypt(&context, random_id)?; - let obj = Object::new(); - for (k, v) in decrypted.iter() { - Reflect::set(&obj, &(*k as u32).into(), &v.into()).unwrap(); - } - Ok(JsValue::from(obj)) - } - - #[wasm_bindgen] - pub async fn answer(&mut self, decision_id: DecisionId, value: String) -> Result<()> { - self.client.answer(decision_id, value).await?; - Ok(()) - } - - /// Get current game state. - pub async fn get_state(&self) -> Result { - let state: String = self - .connection - .get_state(&self.addr, GetStateParams {}) - .await?; - Ok(parse(&state).map_err(|_| Error::JsonParseError)?) - } - - /// Join the game. - #[wasm_bindgen] - pub async fn join(&self, amount: u64) -> Result<()> { - info!("WASM: Join game"); - let game_account = self - .transport - .get_game_account(&self.addr) - .await? - .ok_or(Error::GameAccountNotFound)?; - let count: u16 = game_account.players.len() as _; - - info!(format!("WASM: player number = {}", count)); - if game_account.max_players <= count { - return Err(Error::GameIsFull(count as _))?; - } - - let mut position: Option = None; - for i in 0..game_account.max_players { - if game_account.players.iter().all(|p| p.position != i) { - position = Some(i as _); - break; - } - } - - let position = position.ok_or(Error::GameIsFull(count as _))?; - info!(format!("WASM: player position = {}", position)); - - info!(format!("WASM: join amount = {}", amount)); - self.transport - .join( - &self.wallet, - JoinParams { - game_addr: self.addr.clone(), - amount, - access_version: game_account.access_version, - position: position as _, - }, - ) - .await?; - - Ok(()) - } - - #[wasm_bindgen] - pub async fn exit(&self) -> Result<()> { - info!("WASM: Exit game"); - self.connection - .exit_game(&self.addr, ExitGameParams {}) - .await?; - Ok(()) - } - - #[wasm_bindgen] - pub async fn close(&self) -> Result<()> { - self.exit().await?; - if let Some(event_sub) = self.event_sub.replace(None) { - event_sub.unsubscribe().await?; - } - info!("WASM: App client closed"); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use gloo::console::info; - use serde_json::json; - use wasm_bindgen_test::*; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - async fn test_client() { - let client = AppClient::try_init( - "facade", - "ws://localhost:12002", - "Alice", - "COUNTER_GAME_ADDRESS", - // Function::default(), - ) - .await - .map_err(JsValue::from) - .expect("Failed to create client"); - - info!("Client created"); - - client.attach_game().await; - - // let state = client - // .get_state() - // .await - // .map_err(JsValue::from) - // .expect("Failed to get state"); - } -} diff --git a/legacy/sdk/src/app_helper.rs b/legacy/sdk/src/app_helper.rs deleted file mode 100644 index 45c24c61..00000000 --- a/legacy/sdk/src/app_helper.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! A common client to use in dapp(native version). - -use gloo::console::info; -use gloo::{console::warn, utils::format::JsValueSerdeExt}; -use js_sys::Array; -use js_sys::{Object, Uint8Array}; -use race_core::types::CreatePlayerProfileParams; -use race_transport::TransportLocalT; -use wasm_bindgen::prelude::*; - -use crate::error::Result; -use crate::js::{rget, rget_string, rget_u16, rget_u64}; -use crate::transport::Transport; -use race_core::{ - error::Error, - types::{CreateGameAccountParams, RegisterGameParams}, -}; - -#[wasm_bindgen] -pub struct AppHelper { - transport: Transport, -} - -#[wasm_bindgen] -impl AppHelper { - /// Try initialize an app helper which provides out game functionalities. - /// - /// # Arguments - /// * `chain`, The name of blockchain, currently only `facade` is supported. - /// * `rpc`, The endpoint of blockchain RPC. - #[wasm_bindgen] - pub async fn try_init(transport: JsValue) -> Result { - let transport = Transport::new(transport); - Ok(AppHelper { transport }) - } - - #[wasm_bindgen] - pub async fn get_game_account(&self, game_addr: &str) -> Result { - let game_account = self.transport.get_game_account(game_addr).await?; - Ok(JsValue::from_serde(&game_account).or(Err(Error::JsonParseError))?) - } - - #[wasm_bindgen] - pub async fn create_game_account(&self, wallet: &JsValue, opts: &Object) -> Result { - let title = rget_string(opts, "title")?; - let token_addr = rget_string(opts, "token_addr")?; - let bundle_addr = rget_string(opts, "bundle_addr")?; - let max_players = rget_u16(opts, "max_players")?; - let data: Uint8Array = rget(opts, "data")?; - let min_deposit = rget_u64(opts, "min_deposit")?; - let max_deposit = rget_u64(opts, "max_deposit")?; - let addr = self - .transport - .create_game_account( - wallet, - CreateGameAccountParams { - title, - bundle_addr, - token_addr, - max_players, - data: data.to_vec(), - max_deposit, - min_deposit, - }, - ) - .await?; - Ok(addr) - } - - #[wasm_bindgen] - pub async fn register_game( - &self, - wallet: &JsValue, - game_addr: &str, - reg_addr: &str, - ) -> Result<()> { - self.transport - .register_game( - wallet, - RegisterGameParams { - game_addr: game_addr.to_owned(), - reg_addr: reg_addr.to_owned(), - }, - ) - .await?; - Ok(()) - } - - #[wasm_bindgen] - pub async fn create_profile(&self, wallet: JsValue, nick: &str, pfp: &str) -> Result<()> { - let pfp = if pfp.eq("") { - None - } else { - Some(pfp.to_owned()) - }; - info!(format!("Create profile, nick: {}, pfp: {:?}", nick, pfp)); - self.transport - .create_player_profile( - &wallet, - CreatePlayerProfileParams { - nick: nick.to_owned(), - pfp, - }, - ) - .await?; - info!(format!("Profile created")); - Ok(()) - } - - #[wasm_bindgen] - pub async fn get_profile(&self, addr: &str) -> Result> { - if let Some(p) = self.transport.get_player_profile(addr).await? { - Ok(Some(JsValue::from_serde(&p).unwrap())) - } else { - Ok(None) - } - } - - #[wasm_bindgen] - pub async fn list_games(&self, registration_addrs: Box<[JsValue]>) -> Result { - let games = Array::new(); - for reg_addr in registration_addrs.into_iter() { - if let Some(reg_addr) = JsValue::as_string(reg_addr) { - info!(format!("Reg addr: {:?}", reg_addr)); - if let Some(reg) = self.transport.get_registration(®_addr).await? { - info!(format!("Games: {:?}", reg)); - for game in reg.games { - games.push(&JsValue::from_serde(&game).unwrap()); - } - } else { - warn!(format!("Registration account {} not found!", reg_addr)); - } - } - } - Ok(games) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use serde_json::json; - use wasm_bindgen_test::*; - use web_sys::console::log_1; - - wasm_bindgen_test_configure!(run_in_browser); - - #[wasm_bindgen_test] - async fn test_helper_get_game() { - let app_helper = AppHelper::try_init("facade", "ws://localhost:12002") - .await - .map_err(JsValue::from) - .unwrap(); - let game_account = app_helper - .get_game_account("COUNTER_GAME_ADDRESS") - .await - .map_err(JsValue::from) - .unwrap(); - log_1(&game_account); - } - - #[wasm_bindgen_test] - async fn test_helper_create_game() { - let app_helper = AppHelper::try_init("facade", "ws://localhost:12002") - .await - .map_err(JsValue::from) - .unwrap(); - let data = Uint8Array::new_with_length(8); - data.copy_from(&[1u8; 8]); - - let addr = app_helper - .create_game_account("COUNTER_BUNDLE_ADDRESS".into(), 10, data) - .await - .map_err(JsValue::from) - .unwrap(); - log_1(&JsValue::from_str(&addr)); - - let game_account = app_helper - .get_game_account(&addr) - .await - .map_err(JsValue::from) - .unwrap(); - log_1(&game_account); - - app_helper - .register_game(&addr, "DEFAULT_REGISTRATION_ADDRESS") - .await - .map_err(JsValue::from) - .unwrap(); - } -} diff --git a/legacy/sdk/src/connection.rs b/legacy/sdk/src/connection.rs deleted file mode 100644 index 226c0c87..00000000 --- a/legacy/sdk/src/connection.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! The connection to transactor. - -use std::sync::Arc; - -use async_trait::async_trait; -use futures::lock::Mutex; -use gloo::console::{info, warn}; -use jsonrpsee::{ - core::client::{Client, ClientT, Subscription, SubscriptionClientT}, - rpc_params, - wasm_client::WasmClientBuilder, -}; -use race_core::{ - connection::ConnectionT, - encryptor::EncryptorT, - error::{Error, Result}, - types::{ - AttachGameParams, BroadcastFrame, ExitGameParams, GetStateParams, SubmitEventParams, - SubscribeEventParams, - }, -}; -use race_encryptor::Encryptor; -use serde::{de::DeserializeOwned, Serialize}; - -pub struct Connection { - player_addr: String, - endpoint: String, - encryptor: Arc, - rpc_client: Arc>, - max_retries: u32, -} - -async fn build_rpc_client(endpoint: &str) -> Result { - WasmClientBuilder::default() - .build(endpoint) - .await - .map_err(|e| Error::RpcError(e.to_string())) -} - -#[async_trait] -impl ConnectionT for Connection { - async fn attach_game(&self, game_addr: &str, params: AttachGameParams) -> Result<()> { - self.request("attach_game", game_addr, params).await - } - - async fn submit_event(&self, game_addr: &str, params: SubmitEventParams) -> Result<()> { - self.request("submit_event", game_addr, params).await - } - - async fn exit_game(&self, game_addr: &str, params: ExitGameParams) -> Result<()> { - self.request("exit_game", game_addr, params).await - } -} - -impl Connection { - pub async fn try_new( - player_addr: &str, - endpoint: &str, - encryptor: Arc, - ) -> Result { - info!(format!( - "Establish connection to transactor at {}", - endpoint - )); - let rpc_client = Arc::new(Mutex::new(build_rpc_client(endpoint).await?)); - let max_retries = 3; - Ok(Self { - player_addr: player_addr.into(), - endpoint: endpoint.into(), - encryptor, - rpc_client, - max_retries, - }) - } - - async fn request(&self, method: &str, game_addr: &str, params: P) -> Result - where - P: Serialize + ToString, - R: DeserializeOwned, - { - let mut retried = 0; - let message = format!("{}{}", game_addr, params.to_string()); - // info!("Message to encrypt: {}", &message); - let mut rpc_client = self.rpc_client.lock().await; - loop { - let signature = self - .encryptor - .sign(message.as_bytes(), self.player_addr.clone())?; - let result = rpc_client - .request(method, rpc_params![game_addr, ¶ms, signature]) - .await; - use jsonrpsee::core::error::Error::*; - match result { - Ok(ret) => return Ok(ret), - Err(RestartNeeded(_)) => { - warn!("Disconnected with transactor, will reconnect."); - *rpc_client = build_rpc_client(&self.endpoint).await?; - continue; - } - Err(RequestTimeout) => { - if retried < self.max_retries { - retried += 1; - continue; - } else { - return Err(Error::RpcError(RequestTimeout.to_string())); - } - } - Err(Call(jsonrpsee::types::error::CallError::Failed(e))) => { - warn!("RPC CallError due to", e.to_string()); - match e.downcast_ref::() { - Some(Error::GameNotLoaded) => {} - Some(e) => { - return Err(e.to_owned()); - } - None => { - unreachable!(); - } - } - } - Err(e) => { - warn!("RPC Call failed due to", e.to_string()); - return Err(Error::RpcError(e.to_string())); - } - } - } - } - - pub async fn get_state(&self, game_addr: &str, params: GetStateParams) -> Result - where - R: DeserializeOwned, - { - self.request("get_state", game_addr, params).await - } - - pub async fn subscribe_events( - &self, - game_addr: &str, - settle_version: u64, - // ) -> Result> { - ) -> Result> { - let params = SubscribeEventParams { settle_version }; - let message = format!("{}{}", game_addr, params.to_string()); - let signature = self - .encryptor - .sign(message.as_bytes(), self.player_addr.clone())?; - - let sub = self - .rpc_client - .lock() - .await - .subscribe( - "subscribe_event", - rpc_params![game_addr, params, signature], - "unsubscribe_event", - ) - .await - .map_err(|e| Error::RpcError(e.to_string()))?; - - Ok(sub) - // Ok(stream! { - // for await frame in sub { - // if let Ok(frame) = frame { - // yield frame; - // } else { - // break; - // } - // } - // }) - } -} diff --git a/legacy/sdk/src/error.rs b/legacy/sdk/src/error.rs deleted file mode 100644 index bb0e389f..00000000 --- a/legacy/sdk/src/error.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gloo::console::error; -use race_transport::error::TransportError; -use thiserror::Error; -use wasm_bindgen::prelude::*; - -#[derive(Error, Debug)] -#[allow(unused)] -pub enum SdkError { - #[error("JS invocation error: {0}")] - InteropError(String), - - #[error("Transport error: {0}")] - TransportError(String), - - #[error("Connection(with transactor) error: {0}")] - ConnectionError(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("Internal error: {0}")] - InternalError(String), -} - -impl From for SdkError { - fn from(value: race_core::error::Error) -> Self { - Self::InternalError(value.to_string()) - } -} - -impl From for TransportError { - fn from(value: SdkError) -> Self { - gloo::console::error!("An error occurred in transport:", value.to_string()); - TransportError::InteropError - } -} - -pub type Result = std::result::Result; diff --git a/legacy/sdk/src/handler.rs b/legacy/sdk/src/handler.rs deleted file mode 100644 index 4bb3c6dc..00000000 --- a/legacy/sdk/src/handler.rs +++ /dev/null @@ -1,212 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -use std::mem::swap; -use std::sync::Arc; - -use borsh::{BorshDeserialize, BorshSerialize}; -use race_core::context::GameContext; -use race_core::effect::Effect; -use race_core::encryptor::EncryptorT; -use race_core::engine::{general_handle_event, general_init_state, post_handle_event, InitAccount}; -use race_core::error::{Error, Result}; - -use js_sys::WebAssembly::{Instance, Memory}; -use js_sys::{Function, Object, Reflect, Uint8Array, WebAssembly}; -use race_core::event::Event; -use race_core::types::GameBundle; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; - -pub struct Handler { - pub instance: Instance, - pub encryptor: Arc, -} - -impl Handler { - pub async fn from_bundle(game_bundle: GameBundle, encryptor: Arc) -> Result { - let buffer = game_bundle.data; - let mem_descriptor = Object::new(); - Reflect::set(&mem_descriptor, &"shared".into(), &true.into()).map_err(|_| { - Error::WasmInitializationError("Failed to create mem descriptor".into()) - })?; - Reflect::set(&mem_descriptor, &"maximum".into(), &100.into()).map_err(|_| { - Error::WasmInitializationError("Failed to create mem descriptor".into()) - })?; - Reflect::set(&mem_descriptor, &"initial".into(), &100.into()).map_err(|_| { - Error::WasmInitializationError("Failed to create mem descriptor".into()) - })?; - let memory = WebAssembly::Memory::new(&mem_descriptor).map_err(|_| { - Error::WasmInitializationError("Failed to get WASM memory object".into()) - })?; - let import_obj = Object::new(); - Reflect::set(&import_obj, &"memory".into(), &memory).map_err(|_| { - Error::WasmInitializationError("Failed to set WASM memory object".into()) - })?; - let a = JsFuture::from(WebAssembly::instantiate_buffer(&buffer, &import_obj)) - .await - .map_err(|_| Error::WasmInitializationError("Failed to instantiate buffer".into()))?; - let instance: Instance = Reflect::get(&a, &"instance".into()) - .map_err(|_| Error::WasmInitializationError("Failed to get WASM instance".into()))? - .dyn_into() - .map_err(|_| Error::WasmInitializationError("Failed to get WASM instance".into()))?; - Ok(Self { - instance, - encryptor, - }) - } - - fn custom_init_state( - &self, - context: &mut GameContext, - init_account: &InitAccount, - ) -> Result<()> { - let exports = self.instance.exports(); - let mem = Reflect::get(exports.as_ref(), &"memory".into()) - .map_err(|_| Error::WasmExecutionError("Failed to get memory object".into()))? - .dyn_into::() - .map_err(|_| Error::WasmExecutionError("Failed to get memory object".into()))?; - mem.grow(4); - let buf = Uint8Array::new(&mem.buffer()); - - // serialize effect - let mut effect = Effect::from_context(context); - let effect_vec = effect - .try_to_vec() - .map_err(|_| Error::WasmExecutionError("Failed to serialize effect".into()))?; - let effect_size = effect_vec.len(); - let effect_arr = Uint8Array::new_with_length(effect_size as _); - effect_arr.copy_from(&effect_vec); - - // serialize init_account - let init_account_vec = init_account - .try_to_vec() - .map_err(|_| Error::WasmExecutionError("Failed to serialize init_account".into()))?; - let init_account_size = init_account_vec.len(); - let init_account_arr = Uint8Array::new_with_length(init_account_size as _); - init_account_arr.copy_from(&init_account_vec); - - // copy effect and init_account into wasm memory - let mut offset = 1u32; - buf.set(&effect_arr, offset); - offset += effect_size as u32; - buf.set(&init_account_arr, offset); - - // call event handler - let init_state = Reflect::get(exports.as_ref(), &"init_state".into()) - .map_err(|_| Error::WasmExecutionError("Failed to resolve init_state function".into()))? - .dyn_into::() - .map_err(|_| { - Error::WasmExecutionError("Failed to resolve init_state function".into()) - })?; - - let new_effect_size = init_state - .call2( - &JsValue::undefined(), - &effect_size.into(), - &init_account_size.into(), - ) - .map_err(|_| Error::WasmExecutionError("WASM invocation error".into()))? - .as_f64() - .ok_or(Error::WasmExecutionError( - "WASM result parsing error".into(), - ))?; - - let new_effect_vec = Uint8Array::new(&mem.buffer()).to_vec(); - let new_effect_slice = &new_effect_vec[1..(1 + new_effect_size as usize)]; - effect = Effect::try_from_slice(&new_effect_slice) - .map_err(|_| Error::WasmExecutionError("Failed to deserialize effect".into()))?; - - if let Some(e) = effect.__take_error() { - Err(e) - } else { - context.apply_effect(effect) - } - } - - fn custom_handle_event(&self, context: &mut GameContext, event: &Event) -> Result<()> { - let exports = self.instance.exports(); - let mem = Reflect::get(exports.as_ref(), &"memory".into()) - .map_err(|_| Error::WasmExecutionError("Failed to get memory object".into()))? - .dyn_into::() - .map_err(|_| Error::WasmExecutionError("Failed to get memory object".into()))?; - let buf = Uint8Array::new(&mem.buffer()); - - // serialize effect - let mut effect = Effect::from_context(context); - let effect_vec = effect - .try_to_vec() - .map_err(|_| Error::WasmExecutionError("Failed to serialize effect".into()))?; - let effect_size = effect_vec.len(); - let effect_arr = Uint8Array::new_with_length(effect_size as _); - effect_arr.copy_from(&effect_vec); - - // serialize event - let event_vec = event - .try_to_vec() - .map_err(|_| Error::WasmExecutionError("Failed to serialize event".into()))?; - let event_size = event_vec.len(); - let event_arr = Uint8Array::new_with_length(event_size as _); - event_arr.copy_from(&event_vec); - - // copy context and event into wasm memory - let mut offset = 1u32; - buf.set(&effect_arr, offset); - offset += effect_size as u32; - buf.set(&event_arr, offset); - - // call event handler - let handle_event = Reflect::get(exports.as_ref(), &"handle_event".into()) - .map_err(|_| { - Error::WasmExecutionError("Failed to resolve handle_event function".into()) - })? - .dyn_into::() - .map_err(|_| { - Error::WasmExecutionError("Failed to resolve handle_event function".into()) - })?; - - let new_effect_size = handle_event - .call2( - &JsValue::undefined(), - &effect_size.into(), - &event_size.into(), - ) - .map_err(|_| Error::WasmExecutionError("WASM invocation error".into()))? - .as_f64() - .ok_or(Error::WasmExecutionError( - "WASM result parsing error".into(), - ))?; - - let new_effect_vec = Uint8Array::new(&mem.buffer()).to_vec(); - let new_effect_slice = &new_effect_vec[1..(1 + new_effect_size as usize)]; - effect = Effect::try_from_slice(&new_effect_slice) - .map_err(|_| Error::WasmExecutionError("Failed to deserialize effect".into()))?; - - if let Some(e) = effect.__take_error() { - Err(e) - } else { - context.apply_effect(effect) - } - } - - pub fn handle_event(&self, context: &mut GameContext, event: &Event) -> Result<()> { - let mut new_context = context.clone(); - general_handle_event(&mut new_context, event, self.encryptor.as_ref())?; - self.custom_handle_event(&mut new_context, event)?; - post_handle_event(context, &mut new_context)?; - // TODO, the settlement is ignored - // We should start a independent task to verify the settlements on-chain - // Here we should send a verification job to the task - new_context.apply_and_take_settles()?; - // info!(format!("context: {:?}", new_context)); - swap(context, &mut new_context); - Ok(()) - } - - pub fn init_state(&self, context: &mut GameContext, init_account: &InitAccount) -> Result<()> { - let mut new_context = context.clone(); - general_init_state(&mut new_context, init_account)?; - self.custom_init_state(&mut new_context, init_account)?; - swap(context, &mut new_context); - Ok(()) - } -} diff --git a/legacy/sdk/src/js.rs b/legacy/sdk/src/js.rs deleted file mode 100644 index f546d14f..00000000 --- a/legacy/sdk/src/js.rs +++ /dev/null @@ -1,351 +0,0 @@ -use crate::error::Result; -use gloo::utils::format::JsValueSerdeExt; -use js_sys::{Object, Reflect, JSON::parse}; -use race_core::{ - context::{GameContext, Player, Server}, - types::PlayerJoin, -}; -use serde::Serialize; -use wasm_bindgen::JsValue; -use wasm_bindgen::{prelude::*, JsCast}; - -#[derive(Serialize)] -pub struct JsPlayer<'a> { - pub addr: &'a str, - pub position: usize, - pub status: String, - pub balance: u64, -} - -impl<'a> From<&'a Player> for JsPlayer<'a> { - fn from(value: &'a Player) -> Self { - Self { - addr: &value.addr, - position: value.position, - status: value.status.to_string(), - balance: value.balance, - } - } -} - -#[derive(Serialize)] -pub struct JsPlayerJoin<'a> { - pub addr: &'a str, - pub position: u16, - pub balance: u64, - pub access_version: u64, -} - -impl<'a> From<&'a PlayerJoin> for JsPlayerJoin<'a> { - fn from(value: &'a PlayerJoin) -> Self { - Self { - addr: &value.addr, - position: value.position, - balance: value.balance, - access_version: value.access_version, - } - } -} - -#[derive(Serialize)] -pub struct JsServer<'a> { - pub addr: &'a str, - pub status: String, - pub endpoint: &'a str, -} - -impl<'a> From<&'a Server> for JsServer<'a> { - fn from(value: &'a Server) -> Self { - Self { - addr: &value.addr, - status: value.status.to_string(), - endpoint: &value.endpoint, - } - } -} - -#[derive(Serialize)] -pub struct JsGameContext<'a> { - pub game_addr: &'a str, - pub access_version: u64, - pub settle_version: u64, - pub status: String, - pub allow_exit: bool, - pub players: Vec>, - pub servers: Vec>, -} - -impl<'a> JsGameContext<'a> { - pub(crate) fn from_context(context: &'a GameContext) -> Self { - Self { - game_addr: context.get_game_addr(), - access_version: context.get_access_version(), - settle_version: context.get_settle_version(), - status: context.get_status().to_string(), - allow_exit: context.is_allow_exit(), - servers: context.get_servers().iter().map(Into::into).collect(), - players: context.get_players().iter().map(Into::into).collect(), - } - } -} - -#[wasm_bindgen] -pub struct Event { - kind: String, - sender: Option, - data: JsValue, -} - -#[wasm_bindgen] -impl Event { - #[wasm_bindgen] - pub fn kind(&self) -> Result { - Ok(self.kind.clone()) - } - - #[wasm_bindgen] - pub fn sender(&self) -> Option { - self.sender.clone() - } - - #[wasm_bindgen] - pub fn data(&self) -> JsValue { - self.data.clone() - } -} - -impl From for Event { - fn from(value: race_core::event::Event) -> Self { - use race_core::event::Event::*; - match value { - Custom { sender, raw } => Self { - kind: "custom".into(), - sender: Some(sender), - data: parse(&raw).unwrap(), - }, - Ready => Self { - kind: "ready".into(), - sender: None, - data: JsValue::null(), - }, - ShareSecrets { sender, shares } => { - let data = Object::new(); - Reflect::set( - &data, - &"shares".into(), - &JsValue::from_serde(&shares).unwrap(), - ) - .unwrap(); - Self { - kind: "share-secrets".into(), - sender: Some(sender), - data: JsValue::from(data), - } - } - OperationTimeout { addrs } => { - let data = Object::new(); - Reflect::set( - &data, - &"addrs".into(), - &JsValue::from_serde(&addrs).unwrap(), - ) - .unwrap(); - Self { - kind: "operation-timeout".into(), - sender: None, - data: JsValue::from(data), - } - } - Mask { - sender, - random_id, - ciphertexts, - } => { - let data = Object::new(); - Reflect::set(&data, &"randomId".into(), &random_id.into()).unwrap(); - Reflect::set( - &data, - &"ciphertexts".into(), - &JsValue::from_serde(&ciphertexts).unwrap(), - ) - .unwrap(); - Self { - kind: "mask".into(), - sender: Some(sender), - data: JsValue::from(data), - } - } - Lock { - sender, - random_id, - ciphertexts_and_digests, - } => { - let data = Object::new(); - Reflect::set(&data, &"randomId".into(), &random_id.into()).unwrap(); - Reflect::set( - &data, - &"ciphertextsAndDigests".into(), - &JsValue::from_serde(&ciphertexts_and_digests).unwrap(), - ) - .unwrap(); - Self { - kind: "lock".into(), - sender: Some(sender), - data: JsValue::from(data), - } - } - RandomnessReady { random_id } => { - let data = Object::new(); - Reflect::set(&data, &"randomId".into(), &random_id.into()).unwrap(); - Self { - kind: "randomness-ready".into(), - sender: None, - data: JsValue::from(data), - } - } - Sync { - new_players, - new_servers, - transactor_addr, - access_version, - } => { - let data = Object::new(); - Reflect::set(&data, &"transactorAddr".into(), &transactor_addr.into()).unwrap(); - Reflect::set( - &data, - &"newPlayers".into(), - &JsValue::from_serde(&new_players).unwrap(), - ) - .unwrap(); - Reflect::set( - &data, - &"newServers".into(), - &JsValue::from_serde(&new_servers).unwrap(), - ) - .unwrap(); - Reflect::set(&data, &"accessVersion".into(), &access_version.into()).unwrap(); - Self { - kind: "sync".into(), - sender: None, - data: JsValue::from(data), - } - } - ServerLeave { - server_addr, - transactor_addr, - } => { - let data = Object::new(); - Reflect::set(&data, &"transactorAddr".into(), &transactor_addr.into()).unwrap(); - Reflect::set(&data, &"serverAddr".into(), &server_addr.into()).unwrap(); - Self { - kind: "server-leave".into(), - sender: None, - data: JsValue::from(data), - } - } - Leave { player_addr } => { - let data = Object::new(); - Reflect::set(&data, &"playerAddr".into(), &player_addr.into()).unwrap(); - Self { - kind: "leave".into(), - sender: None, - data: JsValue::from(data), - } - } - GameStart { access_version } => { - let data = Object::new(); - Reflect::set(&data, &"accessVersion".into(), &access_version.into()).unwrap(); - Self { - kind: "game-start".into(), - sender: None, - data: JsValue::from(data), - } - } - WaitingTimeout => Self { - kind: "waiting-timeout".into(), - sender: None, - data: JsValue::null(), - }, - ActionTimeout { player_addr } => { - let data = Object::new(); - Reflect::set(&data, &"playerAddr".into(), &player_addr.into()).unwrap(); - Self { - kind: "action-timeout".into(), - sender: None, - data: JsValue::from(data), - } - } - SecretsReady => Self { - kind: "secrets-ready".into(), - sender: None, - data: JsValue::null(), - }, - Shutdown => Self { - kind: "shutdown".into(), - sender: None, - data: JsValue::null(), - }, - _ => Self { - kind: "unknown".into(), - sender: None, - data: JsValue::null(), - }, - } - } -} - -pub fn rget(obj: &Object, key: &str) -> Result { - let Ok(v) = Reflect::get(obj, &key.to_owned().into()) else { - return Err(JsError::new(&format!("Missing {}", key))) - }; - let Ok(v) = v.dyn_into::() else { - return Err(JsError::new(&format!("Invalid parameter {}", key))) - }; - Ok(v) -} - -pub fn rget_string(obj: &Object, key: &str) -> Result { - let Ok(v) = Reflect::get(obj, &key.to_owned().into()) else { - return Err(JsError::new(&format!("Missing {}", key))) - }; - v.as_string().ok_or(JsError::new(&format!( - "Invalid parameter {}, expect string", - key - ))) -} - -pub fn _rget_u8(obj: &Object, key: &str) -> Result { - let Ok(v) = Reflect::get(obj, &key.to_owned().into()) else { - return Err(JsError::new(&format!("Missing {}", key))) - }; - v.as_f64() - .ok_or(JsError::new(&format!( - "Invalid parameter {}, expect string", - key - ))) - .map(|n| n as u8) -} - -pub fn rget_u16(obj: &Object, key: &str) -> Result { - let Ok(v) = Reflect::get(obj, &key.to_owned().into()) else { - return Err(JsError::new(&format!("Missing {}", key))) - }; - v.as_f64() - .ok_or(JsError::new(&format!( - "Invalid parameter {}, expect string", - key - ))) - .map(|n| n as u16) -} - -pub fn rget_u64(obj: &Object, key: &str) -> Result { - let Ok(v) = Reflect::get(obj, &key.to_owned().into()) else { - return Err(JsError::new(&format!("Missing {}", key))) - }; - v.as_f64() - .ok_or(JsError::new(&format!( - "Invalid parameter {}, expect string", - key - ))) - .map(|n| n as u64) -} diff --git a/legacy/sdk/src/lib.rs b/legacy/sdk/src/lib.rs deleted file mode 100644 index 7ae7c8da..00000000 --- a/legacy/sdk/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![cfg(target_arch = "wasm32")] - -mod app_client; -mod app_helper; -mod connection; -mod error; -mod handler; -mod js; -mod transport; -mod utils; diff --git a/legacy/sdk/src/transport.rs b/legacy/sdk/src/transport.rs deleted file mode 100644 index b5bad79a..00000000 --- a/legacy/sdk/src/transport.rs +++ /dev/null @@ -1,207 +0,0 @@ -#![allow(unused_variables)] -use async_trait::async_trait; -use borsh::BorshDeserialize; -use gloo::utils::format::JsValueSerdeExt; -use js_sys::Uint8Array; -use race_core::types::{ - CloseGameAccountParams, CreateGameAccountParams, CreatePlayerProfileParams, - CreateRegistrationParams, DepositParams, GameAccount, GameBundle, JoinParams, PlayerProfile, - PublishGameParams, RegisterGameParams, RegistrationAccount, ServerAccount, - UnregisterGameParams, VoteParams, -}; -use race_transport::{ - error::{TransportError, TransportResult}, - TransportLocalT, -}; -use serde::Serialize; -use wasm_bindgen::{JsCast, JsValue}; - -use crate::{ - error::SdkError, - utils::{get_function, resolve_promise}, -}; - -pub struct Transport { - inner: JsValue, -} - -impl Transport { - pub fn new(inner: JsValue) -> Self { - Self { inner } - } -} - -fn parse_params(params: &T) -> TransportResult { - JsValue::from_serde(params).map_err(|e| { - TransportError::InvalidParameter( - "Failed to serialize parameters for transport invocation".into(), - ) - }) -} - -fn parse_js_value(value: &JsValue) -> TransportResult> { - if value.is_undefined() { - return Ok(None); - } - let f = get_function(value, "serialize")?; - - let r = match f.call0(value) { - Ok(r) => r, - Err(e) => { - gloo::console::error!("Failed to serialize object:", value, e); - return Err(TransportError::InteropError); - } - }; - - let r = match r.dyn_into::() { - Ok(r) => r, - Err(e) => { - gloo::console::error!("Failed to parse object to Uint8Array:", e); - return Err(TransportError::InteropError); - } - }; - - match T::try_from_slice(&r.to_vec()) { - Ok(r) => Ok(Some(r)), - Err(e) => { - gloo::console::error!("Failed to deserialize:", r); - gloo::console::error!(format!("Error: {:?}", e)); - return Err(TransportError::InteropError); - } - } -} - -impl Transport { - async fn api_fetch( - &self, - api: &str, - addr: &str, - ) -> TransportResult> { - let f = get_function(&self.inner, api)?; - let p = f.call1(&self.inner, &addr.into()).map_err(|e| { - SdkError::InteropError(format!("An error occurred in API fetch: {}", api)) - })?; - let value = resolve_promise(p).await?; - Ok(parse_js_value(&value)?) - } - - async fn api_call( - &self, - api: &str, - wallet: &JsValue, - params: T, - ) -> TransportResult { - let f = get_function(&self.inner, api)?; - let params = parse_params(¶ms)?; - let p: JsValue = f.call2(&self.inner, &wallet, ¶ms).map_err(|e| { - SdkError::InteropError(format!("An error occurred in API call: {}", api)) - })?; - Ok(resolve_promise(p).await?) - } -} - -#[async_trait(?Send)] -impl TransportLocalT for Transport { - async fn create_game_account( - &self, - wallet: &JsValue, - params: CreateGameAccountParams, - ) -> TransportResult { - self.api_call("createGameAccount", wallet, ¶ms) - .await? - .as_string() - .ok_or(TransportError::ParseAddressError) - } - - async fn close_game_account( - &self, - wallet: &JsValue, - params: CloseGameAccountParams, - ) -> TransportResult<()> { - self.api_call("closeGameAccount", wallet, ¶ms).await?; - Ok(()) - } - - async fn create_player_profile( - &self, - wallet: &JsValue, - params: CreatePlayerProfileParams, - ) -> TransportResult<()> { - self.api_call("createPlayerProfile", wallet, ¶ms) - .await?; - Ok(()) - } - - async fn join(&self, wallet: &JsValue, params: JoinParams) -> TransportResult<()> { - self.api_call("join", wallet, ¶ms).await?; - Ok(()) - } - - async fn deposit(&self, wallet: &JsValue, params: DepositParams) -> TransportResult<()> { - self.api_call("deposit", wallet, ¶ms).await?; - Ok(()) - } - - async fn vote(&self, wallet: &JsValue, params: VoteParams) -> TransportResult<()> { - self.api_call("vote", wallet, ¶ms).await?; - Ok(()) - } - - async fn publish_game( - &self, - wallet: &JsValue, - params: PublishGameParams, - ) -> TransportResult { - self.api_call("publishGame", wallet, ¶ms) - .await? - .as_string() - .ok_or(TransportError::ParseAddressError) - } - - async fn create_registration( - &self, - wallet: &JsValue, - params: CreateRegistrationParams, - ) -> TransportResult { - self.api_call("createRegistration", wallet, ¶ms) - .await? - .as_string() - .ok_or(TransportError::ParseAddressError) - } - - async fn register_game( - &self, - wallet: &JsValue, - params: RegisterGameParams, - ) -> TransportResult<()> { - Ok(()) - } - - async fn unregister_game( - &self, - wallet: &JsValue, - _params: UnregisterGameParams, - ) -> TransportResult<()> { - Ok(()) - } - - async fn get_player_profile(&self, addr: &str) -> TransportResult> { - self.api_fetch("getPlayerProfile", addr).await - } - - async fn get_game_account(&self, addr: &str) -> TransportResult> { - self.api_fetch("getGameAccount", addr).await - } - - async fn get_game_bundle(&self, addr: &str) -> TransportResult> { - self.api_fetch("getGameBundle", addr).await - } - - async fn get_server_account(&self, addr: &str) -> TransportResult> { - self.api_fetch("getServerAccount", addr).await - } - - async fn get_registration(&self, addr: &str) -> TransportResult> { - self.api_fetch("getRegistration", addr).await - } -} diff --git a/legacy/sdk/src/utils.rs b/legacy/sdk/src/utils.rs deleted file mode 100644 index f8ed4f36..00000000 --- a/legacy/sdk/src/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -use gloo::console::{error, warn}; -use js_sys::{Function, Promise, Reflect}; -use wasm_bindgen::{JsCast, JsValue}; -use wasm_bindgen_futures::JsFuture; - -use crate::error::SdkError; - -pub(crate) fn rget(obj: &JsValue, key: &str) -> JsValue { - Reflect::get(obj, &key.into()).unwrap() -} - -pub(crate) fn get_function(obj: &JsValue, name: &str) -> Result { - Reflect::get(obj, &name.into()) - .map_err(|_e| { - error!("Object:", obj); - error!("Property:", name); - SdkError::InteropError("Function does not exist".into()) - })? - .dyn_into::() - .map_err(|_e| { - error!("Object:", obj); - error!("Property:", name); - SdkError::InteropError("Property is not a function".into()) - }) -} - -pub(crate) async fn resolve_promise(p: JsValue) -> Result { - let p = match p.dyn_into::() { - Ok(p) => p, - Err(e) => { - warn!("Unexpected type of JsValue(Promise was expected):", e); - return Err(SdkError::InteropError("Unexpected type of JsValue".into())); - } - }; - match JsFuture::from(p).await { - Ok(x) => Ok(x), - Err(e) => { - warn!("Failed to resolve promise:", e); - return Err(SdkError::InteropError("Failed to resolve a promise".into())); - } - } -} diff --git a/local-db/Cargo.toml b/local-db/Cargo.toml new file mode 100644 index 00000000..2b941152 --- /dev/null +++ b/local-db/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "race-local-db" +description = "Module for save data to local database." +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +documentation.workspace = true +homepage.workspace = true +keywords.workspace = true +readme.workspace = true +publish = true + +[dependencies] +race-core = { workspace = true } +race-api = { workspace = true } +race-transport = { workspace = true } +race-env = { workspace = true } +tokio = { workspace = true, features = ["full"] } +anyhow = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +borsh = { workspace = true } +async-trait = { workspace = true } +rusqlite = { workspace = true, features = ["bundled"] } +sha256 = { workspace = true } diff --git a/local-db/src/lib.rs b/local-db/src/lib.rs new file mode 100644 index 00000000..33715dfd --- /dev/null +++ b/local-db/src/lib.rs @@ -0,0 +1,132 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use borsh::BorshDeserialize; +use race_api::error::{Error, Result}; +use race_core::{ + checkpoint::CheckpointOffChain, + storage::StorageT, + types::{GetCheckpointParams, SaveCheckpointParams}, +}; +use rusqlite::{params, Connection, OptionalExtension}; +use tokio::sync::Mutex; +use sha256::digest; + +pub struct LocalDbStorage { + conn: Arc>, +} + +#[async_trait] +impl StorageT for LocalDbStorage { + async fn save_checkpoint(&self, params: SaveCheckpointParams) -> Result<()> { + let conn = self.conn.lock().await; + let checkpoint_bs = borsh::to_vec(¶ms.checkpoint).or(Err(Error::MalformedCheckpoint))?; + let sha = digest(&checkpoint_bs); + conn.execute( + "INSERT OR REPLACE INTO game_checkpoints (game_addr, settle_version, checkpoint, sha) VALUES (?1, ?2, ?3, ?4)", + params![params.game_addr, params.settle_version, checkpoint_bs, sha], + ) + .map_err(|e| Error::StorageError(e.to_string()))?; + + Ok(()) + } + + async fn get_checkpoint( + &self, + params: GetCheckpointParams, + ) -> Result> { + let conn = self.conn.lock().await; + + let checkpoint_bs = conn + .query_row( + "SELECT checkpoint FROM game_checkpoints WHERE game_addr = ?1 and settle_version = ?2", + params![params.game_addr, params.settle_version], + |row| { + row.get::<_, Vec>(0) + } + ).optional() + .map_err(|e| Error::StorageError(e.to_string()))?; + + if let Some(checkpoint_bs) = checkpoint_bs { + let checkpoint = CheckpointOffChain::try_from_slice(&checkpoint_bs) + .map_err(|e| Error::StorageError(e.to_string()))?; + + Ok(Some(checkpoint)) + } else { + Ok(None) + } + } +} + +pub fn init_table(conn: &Connection) -> Result<()> { + conn.execute( + "CREATE TABLE IF NOT EXISTS game_checkpoints ( + game_addr TEXT NOT NULL, + settle_version INTEGER NOT NULL, + checkpoint BLOB NOT NULL, + sha TEXT NOT NULL, + PRIMARY KEY(game_addr, settle_version) + )", + (), + ) + .map_err(|e| Error::StorageError(e.to_string()))?; + Ok(()) +} + +impl LocalDbStorage { + pub fn try_new_mem() -> Result { + let conn = Connection::open_in_memory().map_err(|e| Error::StorageError(e.to_string()))?; + + init_table(&conn)?; + + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + pub fn try_new(db_file_path: &str) -> Result { + let conn = + Connection::open(db_file_path).map_err(|e| Error::StorageError(e.to_string()))?; + + init_table(&conn)?; + + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } +} + +#[cfg(test)] +mod tests { + use race_core::checkpoint::Checkpoint; + + use super::*; + + #[tokio::test] + async fn test_insert_and_query() { + let game_addr = "testaddr1".to_string(); + let state = vec![1, 2, 3, 4]; + let settle_version = 10; + // let storage = LocalDbStorage::try_new("trasnactor-db").unwrap(); + let storage = LocalDbStorage::try_new_mem().unwrap(); + let checkpoint = Checkpoint::new(0, 1, 1, state).derive_offchain_part(); + storage + .save_checkpoint(SaveCheckpointParams { + game_addr: game_addr.clone(), + settle_version, + checkpoint: checkpoint.clone(), + }) + .await + .unwrap(); + + let checkpoint_from_db = storage + .get_checkpoint(GetCheckpointParams { + game_addr: game_addr.clone(), + settle_version, + }) + .await + .unwrap(); + + assert_eq!(checkpoint_from_db, Some(checkpoint)); + } +} diff --git a/proc-macro/src/lib.rs b/proc-macro/src/lib.rs index 1bda3e43..b9c33911 100644 --- a/proc-macro/src/lib.rs +++ b/proc-macro/src/lib.rs @@ -15,24 +15,15 @@ use syn::{parse_macro_input, ItemStruct}; /// #[game_handler] /// struct S {} /// -/// #[derive(BorshDeserialize, BorshSerialize)] -/// struct Checkpoint {} -/// /// impl GameHandler for S { /// -/// type Checkpoint = Checkpoint; -/// /// fn init_state(context: &mut Effect, init_account: InitAccount) -> HandleResult { /// Ok(Self {}) /// } - +/// /// fn handle_event(&mut self, context: &mut Effect, event: Event) -> HandleResult<()> { /// Ok(()) /// } -/// -/// fn into_checkpoint(self) -> HandleResult { -/// Ok(Checkpoint {}) -/// } /// } /// ``` #[proc_macro_attribute] @@ -55,7 +46,7 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { } pub fn write_ptr(ptr: &mut *mut u8, data: T) -> u32 { - if let Ok(vec) = data.try_to_vec() { + if let Ok(vec) = borsh::to_vec(&data) { unsafe { std::ptr::copy(vec.as_ptr(), *ptr, vec.len()) } vec.len() as _ } else { @@ -83,13 +74,6 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { Err(e) => effect.__set_error(e), } - if effect.is_checkpoint { - match handler.into_checkpoint() { - Ok(checkpoint_state) => effect.__set_checkpoint(checkpoint_state), - Err(e) => effect.__set_error(e), - } - } - let mut ptr = 1 as *mut u8; write_ptr(&mut ptr, effect) } @@ -102,7 +86,7 @@ pub fn game_handler(_metadata: TokenStream, input: TokenStream) -> TokenStream { } else { return 1 }; - let init_account: race_api::engine::InitAccount = if let Some(init_account) = read_ptr(&mut ptr, init_account_size) { + let init_account: race_api::init_account::InitAccount = if let Some(init_account) = read_ptr(&mut ptr, init_account_size) { init_account } else { return 2 diff --git a/test/Cargo.toml b/test/Cargo.toml index b3e173c7..58eca3fa 100644 --- a/test/Cargo.toml +++ b/test/Cargo.toml @@ -20,6 +20,8 @@ race-client.workspace = true race-encryptor.workspace = true borsh.workspace = true async-trait.workspace = true +async-stream.workspace = true +futures.workspace = true rand.workspace = true serde_json.workspace = true serde.workspace = true diff --git a/test/src/account_helpers.rs b/test/src/account_helpers.rs index eb6fbd6f..8ae4bc94 100644 --- a/test/src/account_helpers.rs +++ b/test/src/account_helpers.rs @@ -1,10 +1,6 @@ -use crate::client_helpers::TestClient; +use crate::{client_helpers::TestClient, misc::{test_game_addr, test_game_title}}; use borsh::BorshSerialize; -use race_core::types::{ClientMode, EntryType, GameAccount, GameBundle, PlayerJoin, ServerJoin}; - -pub fn test_game_addr() -> String { - "TEST".into() -} +use race_core::types::{ClientMode, EntryType, GameAccount, PlayerJoin, ServerJoin}; pub struct TestGameAccountBuilder { account: GameAccount, @@ -13,8 +9,8 @@ pub struct TestGameAccountBuilder { impl Default for TestGameAccountBuilder { fn default() -> Self { let account = GameAccount { - addr: "TEST".into(), - title: "Unnamed".into(), + addr: test_game_addr(), + title: test_game_title(), bundle_addr: "".into(), owner_addr: "".into(), settle_version: 0, @@ -31,8 +27,7 @@ impl Default for TestGameAccountBuilder { recipient_addr: "".into(), entry_type: EntryType::default(), token_addr: "".into(), - checkpoint: vec![], - checkpoint_access_version: 0, + checkpoint_on_chain: None, }; TestGameAccountBuilder { account } } @@ -72,8 +67,8 @@ impl TestGameAccountBuilder { self } - pub fn set_transactor(mut self, server: &TestClient) -> Self { - if server.get_mode().ne(&ClientMode::Transactor) { + pub fn set_transactor(mut self, server: &mut TestClient) -> Self { + if server.mode().ne(&ClientMode::Transactor) { panic!("A test client in TRANSACTOR Mode is required"); } if self.account.transactor_addr.is_some() { @@ -83,49 +78,51 @@ impl TestGameAccountBuilder { .account .servers .iter() - .find(|s| s.addr.eq(&server.get_addr())) + .find(|s| s.addr.eq(&server.addr())) .is_some() { panic!("Server already added") } - self.account.transactor_addr = Some(server.get_addr()); + self.account.transactor_addr = Some(server.addr()); self.account.access_version += 1; self.account.servers.insert( 0, ServerJoin { - addr: server.get_addr(), + addr: server.addr(), endpoint: "".into(), access_version: self.account.access_version, verify_key: "".into(), }, ); + server.set_id(self.account.access_version); self } - pub fn add_validator(mut self, server: &TestClient) -> Self { - if server.get_mode().ne(&ClientMode::Validator) { + pub fn add_validator(mut self, server: &mut TestClient) -> Self { + if server.mode().ne(&ClientMode::Validator) { panic!("A test client in VALIDATOR Mode is required"); } if self .account .servers .iter() - .find(|s| s.addr.eq(&server.get_addr())) + .find(|s| s.addr.eq(&server.addr())) .is_some() { panic!("Server already added") } self.account.access_version += 1; self.account.servers.push(ServerJoin { - addr: server.get_addr(), + addr: server.addr(), endpoint: "".into(), access_version: self.account.access_version, verify_key: "".into(), }); + server.set_id(self.account.access_version); self } - pub fn add_player(self, player: &TestClient, deposit: u64) -> Self { + pub fn add_player(self, player: &mut TestClient, deposit: u64) -> Self { let mut position = None; for i in 0..self.account.max_players { if self @@ -150,7 +147,7 @@ impl TestGameAccountBuilder { pub fn add_player_with_position( mut self, - player: &TestClient, + player: &mut TestClient, deposit: u64, position: u16, ) -> Self { @@ -158,12 +155,12 @@ impl TestGameAccountBuilder { .account .players .iter() - .find(|p| p.addr.eq(&player.get_addr())) + .find(|p| p.addr.eq(&player.addr())) .is_some() { panic!("Player already added") } - if player.get_mode().ne(&ClientMode::Player) { + if player.mode().ne(&ClientMode::Player) { panic!("A test client in PLAYER mode is required"); } self.account.access_version += 1; @@ -176,17 +173,18 @@ impl TestGameAccountBuilder { panic!("Player position occupied"); } self.account.players.push(PlayerJoin { - addr: player.get_addr(), + addr: player.addr(), position, access_version: self.account.access_version, balance: deposit, verify_key: "".into(), }); + player.set_id(self.account.access_version); self } pub fn with_data(self, account_data: T) -> Self { - let data = account_data.try_to_vec().expect("Serialize data failed"); + let data = borsh::to_vec(&account_data).expect("Serialize data failed"); self.with_data_vec(data) } @@ -196,14 +194,3 @@ impl TestGameAccountBuilder { self } } - -pub fn create_game_bundle_from_path(path: &str) -> GameBundle { - let proj_root = project_root::get_project_root().expect("No project root found"); - let bundle_path = proj_root.join(path); - let data = std::fs::read(bundle_path).expect("Can't read file"); - GameBundle { - uri: "".to_string(), - name: "FILE BUNDLE".to_string(), - data, - } -} diff --git a/test/src/client_helpers.rs b/test/src/client_helpers.rs index 3326c7d8..ac422d98 100644 --- a/test/src/client_helpers.rs +++ b/test/src/client_helpers.rs @@ -1,9 +1,11 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; +use race_api::types::{GamePlayer, PlayerJoin}; use race_client::Client; -use race_api::error::Result; +use race_api::error::{Result, Error}; use race_api::event::{CustomEvent, Event}; +use race_core::types::GameAccount; use race_core::{ connection::ConnectionT, context::GameContext, @@ -13,12 +15,24 @@ use race_core::{ use race_encryptor::Encryptor; use tokio::sync::{mpsc, Mutex}; -use crate::{transport_helpers::DummyTransport, account_helpers::test_game_addr}; +use crate::misc::test_game_addr; +use crate::transport_helpers::DummyTransport; pub struct TestClient { + id: Option, client: Client, } +pub(crate) trait AsGameContextRef { + fn as_game_context_ref(&self) -> &GameContext; +} + +impl AsGameContextRef for GameContext { + fn as_game_context_ref(&self) -> &GameContext { + &self + } +} + pub struct DummyConnection { rx: Mutex>, tx: mpsc::Sender, @@ -69,6 +83,7 @@ impl TestClient { let encryptor = Arc::new(Encryptor::default()); let connection = Arc::new(DummyConnection::default()); Self { + id: None, client: Client::new( addr, test_game_addr(), @@ -84,6 +99,45 @@ impl TestClient { Self::new(addr, ClientMode::Player) } + pub(crate) fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + pub(crate) fn join(&mut self, game_context: &mut GameContext, game_account: &mut GameAccount, balance: u64) -> Result { + if self.client.mode != ClientMode::Player { + panic!("TestClient can only join with Player mode"); + } + + if game_account.players.len() >= game_account.max_players as _ { + return Err(Error::GameIsFull(game_account.max_players as _)) + } + + game_account.access_version += 1; + let id = game_account.access_version; + + let mut position = 0; + for i in 0..game_account.max_players { + if game_account.players.iter().find(|p| p.position == i).is_none() { + position = i; + break; + } + } + + game_account.players.push(PlayerJoin { + addr: self.client.addr.clone(), + position, + balance, + access_version: id, + verify_key: "".into(), + }); + self.set_id(id); + game_context.add_node(self.client.addr.clone(), id, ClientMode::Player); + + Ok(GamePlayer { + id, position, balance + }) + } + pub fn transactor>(addr: S) -> Self { Self::new(addr, ClientMode::Transactor) } @@ -92,34 +146,38 @@ impl TestClient { Self::new(addr, ClientMode::Validator) } - pub fn handle_updated_context(&mut self, ctx: &GameContext) -> Result> { - self.client.handle_updated_context(ctx) + pub(crate) fn handle_updated_context(&mut self, ctx: &T) -> Result> { + self.client.handle_updated_context(ctx.as_game_context_ref()) } - pub fn get_mode(&self) -> ClientMode { + pub fn mode(&self) -> ClientMode { self.client.mode.clone() } - pub fn get_addr(&self) -> String { + pub fn addr(&self) -> String { self.client.addr.clone() } - pub fn decrypt( - &mut self, - ctx: &GameContext, + pub(crate) fn decrypt( + &self, + ctx: &T, random_id: usize, ) -> Result> { - self.client.decrypt(ctx, random_id) + self.client.decrypt(ctx.as_game_context_ref(), random_id) } pub fn secret_state(&self) -> &SecretState { &self.client.secret_state } + pub fn id(&self) -> u64 { + self.id.expect(&format!("Client {} is not in game", self.client.addr)) + } + pub fn custom_event(&self, custom_event: E) -> Event { Event::Custom { - sender: self.client.addr.to_owned(), - raw: custom_event.try_to_vec().unwrap(), + sender: self.id(), + raw: borsh::to_vec(&custom_event).unwrap(), } } @@ -135,7 +193,7 @@ mod tests { #[tokio::test] async fn test_dummy_connection() -> Result<()> { let conn = DummyConnection::default(); - let event = Event::GameStart { access_version: 1 }; + let event = Event::GameStart; conn.submit_event( "", SubmitEventParams { diff --git a/test/src/context_helpers.rs b/test/src/context_helpers.rs new file mode 100644 index 00000000..0b1aa97b --- /dev/null +++ b/test/src/context_helpers.rs @@ -0,0 +1,305 @@ +use std::collections::HashMap; + +use crate::client_helpers::TestClient; +use crate::misc::{test_game_addr, test_game_title}; +use crate::prelude::{AsGameContextRef, TestHandler}; +use borsh::BorshSerialize; +use race_api::engine::GameHandler; +use race_api::error::Result; +use race_api::event::Event; +use race_api::prelude::InitAccount; +use race_api::random::RandomState; +use race_core::checkpoint::{CheckpointOffChain, CheckpointOnChain, VersionedData}; +use race_core::types::{ClientMode, EntryType, GameAccount, PlayerJoin, ServerJoin}; +use race_core::context::{DispatchEvent, EventEffects, GameContext}; + +pub struct TestContext +where + H: GameHandler, +{ + context: GameContext, + account: GameAccount, + handler: TestHandler, +} + +impl TestContext { + pub fn join(&mut self, player: &mut TestClient, deposit: u64) -> Event { + self.join_multi(vec![(player, deposit)]) + } + + pub fn join_multi(&mut self, players_and_deposits: Vec<(&mut TestClient, u64)>) -> Event { + let mut players = vec![]; + for (test_client, deposit) in players_and_deposits.into_iter() { + players.push( + test_client + .join(&mut self.context, &mut self.account, deposit) + .expect("Add player to TestContext"), + ); + } + Event::Join { players } + } + + pub fn handle_event(&mut self, event: &Event) -> Result { + self.handler.handle_event(&mut self.context, event) + } + + pub fn handle_dispatch_event(&mut self) -> Result { + self.handler.handle_dispatch_event(&mut self.context) + } + + pub fn handle_dispatch_until_no_events( + &mut self, + clients: Vec<&mut TestClient>, + ) -> Result { + self.handler + .handle_dispatch_until_no_events(&mut self.context, clients) + } + + pub fn init_account(&self) -> Result { + self.context.init_account() + } + + pub fn state(&self) -> &H { + self.handler.state() + } + + pub fn state_mut(&mut self) -> &mut H { + self.handler.state_mut() + } + + pub fn random_state(&mut self, random_id: usize) -> Result<&RandomState> { + self.context.get_random_state(random_id) + } + + pub fn random_state_mut(&mut self, random_id: usize) -> Result<&mut RandomState> { + self.context.get_random_state_mut(random_id) + } + + pub fn current_dispatch(&self) -> Option { + self.context.get_dispatch().clone() + } + + pub fn client_events(&self, client: &mut TestClient) -> Result> { + client.handle_updated_context(self) + } + + pub fn client_decrypt(&self, client: &TestClient, random_id: usize) -> Result> { + client.decrypt(self, random_id) + } +} + +pub struct TestContextBuilder { + account: GameAccount, +} + +impl Default for TestContextBuilder { + fn default() -> Self { + let account = GameAccount { + addr: test_game_addr(), + title: test_game_title(), + bundle_addr: "".into(), + owner_addr: "".into(), + settle_version: 0, + access_version: 0, + players: vec![], + data_len: 0, + data: vec![], + transactor_addr: None, + servers: vec![], + votes: vec![], + unlock_time: None, + max_players: 6, + deposits: vec![], + recipient_addr: "".into(), + entry_type: EntryType::default(), + token_addr: "".into(), + checkpoint_on_chain: None, + }; + TestContextBuilder { account } + } +} + +impl TestContextBuilder { + /// Initialize by calling handler's `init_state` without checkpoint. + pub fn build_with_init_state(self) -> Result<(TestContext, EventEffects)> { + let mut context = GameContext::try_new(&self.account, None) + .expect("Create game context with initial state api"); + context.set_node_ready(context.access_version()); + + let (handler, event_effects) = TestHandler::::init_state(&mut context)?; + + Ok(( + TestContext { + context, + account: self.account, + handler, + }, + event_effects, + )) + } + + /// Initialize with handler's checkpoint state. + pub fn build_with_checkpoint( + mut self, + checkpoint: &H, + ) -> Result<(TestContext, EventEffects)> { + let mut checkpoint_on_chain = CheckpointOnChain::default(); + let mut checkpoint_off_chain = CheckpointOffChain::default(); + + checkpoint_on_chain.access_version = self.account.access_version; + self.account.checkpoint_on_chain = Some(checkpoint_on_chain); + + checkpoint_off_chain.data.insert( + 0, + VersionedData { + id: 0, + version: 1, + data: borsh::to_vec(checkpoint).expect("Failed to serialize checkpoint"), + sha: vec![], + }, + ); + + let mut context = GameContext::try_new(&self.account, Some(checkpoint_off_chain)) + .expect("Create game context with checkpoint state"); + + let (handler, event_effects) = TestHandler::::init_state(&mut context)?; + + Ok(( + TestContext { + context, + account: self.account, + handler, + }, + event_effects, + )) + } + + pub fn with_max_players(mut self, max_players: u16) -> Self { + if max_players < self.account.players.len() as _ { + panic!("Invalid max_players specified, more players were added"); + } + self.account.max_players = max_players; + self + } + + pub fn with_deposit_range(mut self, min: u64, max: u64) -> Self { + if max < min { + panic!("Invalid deposit value, the max must be greater than the min"); + } + self.account.entry_type = EntryType::Cash { + max_deposit: max, + min_deposit: min, + }; + self + } + + pub fn set_transactor(mut self, server: &mut TestClient) -> Self { + if server.mode().ne(&ClientMode::Transactor) { + panic!("A test client in TRANSACTOR Mode is required"); + } + if self.account.transactor_addr.is_some() { + panic!("Only one transactor is allowed"); + } + if self + .account + .servers + .iter() + .find(|s| s.addr.eq(&server.addr())) + .is_some() + { + panic!("Server already added") + } + self.account.transactor_addr = Some(server.addr()); + self.account.access_version += 1; + self.account.servers.insert( + 0, + ServerJoin { + addr: server.addr(), + endpoint: "".into(), + access_version: self.account.access_version, + verify_key: "".into(), + }, + ); + server.set_id(self.account.access_version); + self + } + + pub fn add_player(self, player: &mut TestClient, deposit: u64) -> Self { + let mut position = None; + for i in 0..self.account.max_players { + if self + .account + .players + .iter() + .find(|p| p.position == i) + .is_some() + { + continue; + } else { + position = Some(i); + break; + } + } + if let Some(position) = position { + self.add_player_with_position(player, deposit, position) + } else { + panic!("Can't add player, game account is full"); + } + } + + pub fn add_player_with_position( + mut self, + player: &mut TestClient, + deposit: u64, + position: u16, + ) -> Self { + if self + .account + .players + .iter() + .find(|p| p.addr.eq(&player.addr())) + .is_some() + { + panic!("Player already added") + } + if player.mode().ne(&ClientMode::Player) { + panic!("A test client in PLAYER mode is required"); + } + self.account.access_version += 1; + for p in self.account.players.iter() { + if p.position == position { + panic!("Player position occupied"); + } + } + if position >= self.account.max_players { + panic!("Player position occupied"); + } + self.account.players.push(PlayerJoin { + addr: player.addr(), + position, + access_version: self.account.access_version, + balance: deposit, + verify_key: "".into(), + }); + player.set_id(self.account.access_version); + self + } + + pub fn with_data(self, account_data: T) -> Self { + let data = borsh::to_vec(&account_data).expect("Serialize data failed"); + self.with_data_vec(data) + } + + pub fn with_data_vec(mut self, data: Vec) -> Self { + self.account.data_len = data.len() as _; + self.account.data = data; + self + } +} + + +impl AsGameContextRef for TestContext { + fn as_game_context_ref(&self) -> &GameContext { + &self.context + } +} diff --git a/test/src/event_helpers.rs b/test/src/event_helpers.rs deleted file mode 100644 index 03e1c6ae..00000000 --- a/test/src/event_helpers.rs +++ /dev/null @@ -1,34 +0,0 @@ -use race_api::event::Event; -use race_api::types::PlayerJoin; - -type NewPlayerSpec<'a> = (&'a str, u16, u64); - -pub fn sync_new_players(addr_pos_balance_list: &[NewPlayerSpec], access_version: u64) -> Event { - let mut new_players: Vec = Vec::default(); - - for (addr, pos, balance) in addr_pos_balance_list.iter() { - if new_players.iter().find(|p| p.addr.eq(addr)).is_some() { - panic!("Duplicated address: {}", addr) - } - if *balance == 0 { - panic!("Zero balance: {}", addr) - } - if new_players.iter().find(|p| p.position.eq(pos)).is_some() { - panic!("Duplicated position: {} at {}", addr, pos) - } - new_players.push(PlayerJoin { - addr: addr.to_string(), - position: *pos, - balance: *balance, - verify_key: "".to_string(), - access_version, - }); - } - - Event::Sync { - new_players, - new_servers: Default::default(), - transactor_addr: "".to_string(), - access_version, - } -} diff --git a/test/src/handler_helpers.rs b/test/src/handler_helpers.rs index 38b4bfb2..9ca3a395 100644 --- a/test/src/handler_helpers.rs +++ b/test/src/handler_helpers.rs @@ -3,19 +3,18 @@ use std::mem::swap; use race_api::engine::GameHandler; use race_api::error::Result; use race_api::event::Event; -use race_api::effect::Effect; -use race_core::context::GameContext; -use race_core::engine::{general_handle_event, general_init_state}; -use race_core::types::GameAccount; +use race_core::context::{EventEffects, GameContext}; +use race_core::engine::general_handle_event; use race_encryptor::Encryptor; use crate::client_helpers::TestClient; -fn parse_effect_checkpoint(effect: &mut Effect) -> Result<()> { - if effect.is_checkpoint { - effect.__set_checkpoint_raw(vec![]); + +// Some event has special handling in event loop. +fn patch_handle_event_effects(context: &mut GameContext, event_effects: &EventEffects) { + if event_effects.start_game { + context.dispatch_safe(Event::GameStart, 0); } - Ok(()) } /// A wrapped handler for testing @@ -28,41 +27,58 @@ where } impl TestHandler { - pub fn init_state(context: &mut GameContext, game_account: &GameAccount) -> Result { + pub fn init_state(context: &mut GameContext) -> Result<(Self, EventEffects)> { let mut new_context = context.clone(); - let init_account = game_account.derive_init_account(); - general_init_state(&mut new_context, &init_account)?; - let mut effect = new_context.derive_effect(); + let init_account = new_context.init_account()?; + let mut effect = new_context.derive_effect(true); let handler = H::init_state(&mut effect, init_account)?; - new_context.apply_effect(effect)?; + let event_effects = new_context.apply_effect(effect)?; + patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); - Ok(Self { handler }) + Ok((Self { handler }, event_effects)) } - pub fn handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result<()> { + pub fn handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result { let mut new_context = context.clone(); let encryptor = Encryptor::default(); general_handle_event(&mut new_context, event, &encryptor)?; - let mut effect = new_context.derive_effect(); + let mut effect = new_context.derive_effect(false); self.handler.handle_event(&mut effect, event.to_owned())?; - parse_effect_checkpoint::(&mut effect)?; - new_context.apply_effect(effect)?; + let event_effects = new_context.apply_effect(effect)?; + patch_handle_event_effects(&mut new_context, &event_effects); swap(context, &mut new_context); - Ok(()) + Ok(event_effects) } /// Find the event which is going to be disptached in the context, then process it. /// In real cases, the disptached event will be handled by an event loop. /// We use this function to simulate such cases, since we don't have an event loop in tests. - pub fn handle_dispatch_event(&mut self, context: &mut GameContext) -> Result<()> { - let event = context + pub fn handle_dispatch_event(&mut self, context: &mut GameContext) -> Result { + let evt = context + .get_dispatch() + .as_ref() + .expect("No dispatch event") + .event + .clone(); + context.cancel_dispatch(); + println!("* Dispatch event: {}", evt); + self.handle_event(context, &evt) + } + + pub fn handle_dispatch_until_no_events( + &mut self, + context: &mut GameContext, + clients: Vec<&mut TestClient>, + ) -> Result { + let evt = context .get_dispatch() .as_ref() .expect("No dispatch event") .event .clone(); - self.handle_event(context, &event)?; - Ok(()) + context.cancel_dispatch(); + println!("* Dispatch event: {}", evt); + self.handle_until_no_events(context, &evt, clients) } /// This fn keeps handling events of the following two types, until there is none: @@ -73,18 +89,19 @@ impl TestHandler { context: &mut GameContext, event: &Event, mut clients: Vec<&mut TestClient>, - ) -> Result<()> { + ) -> Result { // 1. Process the `event'(arg) --> context updated // 2. context may dispatch --> take care those with timeout == current timestamp // 3. iter clients to syn with updated context --> a couple of events // 4. handle these client/trans events let mut evts: Vec = vec![event.clone()]; // keep handling events in this vec + let mut event_effects = EventEffects::default(); while !evts.is_empty() { let evt = &evts[0]; println!("* Received event: {}", evt); - self.handle_event(context, evt)?; + event_effects = self.handle_event(context, evt)?; if evts.len() == 1 { evts.clear(); } else { @@ -107,14 +124,14 @@ impl TestHandler { println!("* Context dispatch: {:?}", dispatch); } } - Ok(()) + Ok(event_effects) } - pub fn get_state(&self) -> &H { + pub fn state(&self) -> &H { &self.handler } - pub fn get_mut_state(&mut self) -> &mut H { + pub fn state_mut(&mut self) -> &mut H { &mut self.handler } } diff --git a/test/src/lib.rs b/test/src/lib.rs index cd1c476b..03fa8515 100644 --- a/test/src/lib.rs +++ b/test/src/lib.rs @@ -2,5 +2,6 @@ mod account_helpers; mod client_helpers; mod handler_helpers; mod transport_helpers; -mod event_helpers; +mod context_helpers; +mod misc; pub mod prelude; diff --git a/test/src/misc.rs b/test/src/misc.rs new file mode 100644 index 00000000..019ad752 --- /dev/null +++ b/test/src/misc.rs @@ -0,0 +1,8 @@ + +pub fn test_game_addr() -> String { + "TEST".into() +} + +pub fn test_game_title() -> String { + "Unnamed Test Game".into() +} diff --git a/test/src/prelude.rs b/test/src/prelude.rs index 3eab6c55..da9d4aa8 100644 --- a/test/src/prelude.rs +++ b/test/src/prelude.rs @@ -1,10 +1,12 @@ pub use crate::account_helpers::*; pub use crate::client_helpers::*; -pub use crate::event_helpers::*; pub use crate::handler_helpers::*; pub use crate::transport_helpers::*; +pub use crate::context_helpers::*; +pub use crate::misc::*; pub use race_api::error::{Error, Result}; pub use race_api::types::{Settle, SettleOp, Transfer}; pub use race_core::context::{DispatchEvent, GameContext}; -pub use race_core::types::GameAccount; +pub use race_core::types::{GameAccount, ClientMode}; +pub use race_api::effect::{SubGame, EmitBridgeEvent}; diff --git a/test/src/test_context.rs b/test/src/test_context.rs new file mode 100644 index 00000000..2c64935e --- /dev/null +++ b/test/src/test_context.rs @@ -0,0 +1,6 @@ +use race_core::{context::GameContext, types::GameAccount}; + +pub struct TestContext { + game_context: GameContext, + game_account: GameAccount, +} diff --git a/test/src/transport_helpers.rs b/test/src/transport_helpers.rs index c3e1569d..c2d1d774 100644 --- a/test/src/transport_helpers.rs +++ b/test/src/transport_helpers.rs @@ -1,12 +1,12 @@ use std::{ - io::Read, - ops::Deref, - sync::{Arc, Mutex}, + io::Read, ops::Deref, pin::Pin, sync::{Arc, Mutex} }; use async_trait::async_trait; +use async_stream::stream; +use futures::Stream; use base64::prelude::Engine; -use race_core::types::{CreateRecipientParams, AssignRecipientParams, RecipientAccount, RecipientClaimParams}; +use race_core::types::{CreateRecipientParams, AssignRecipientParams, RecipientAccount, RecipientClaimParams, SettleWithAddr}; use race_api::error::{Error, Result}; #[allow(unused_imports)] use race_core::{ @@ -21,14 +21,14 @@ use race_core::{ }; pub struct DummyTransport { - settles: Arc>>, + settles: Arc>>, states: Arc>>, fail_next_settle: Arc>, } impl DummyTransport { #[allow(dead_code)] - pub fn get_settles(&self) -> impl Deref> + '_ { + pub fn get_settles(&self) -> impl Deref> + '_ { self.settles.lock().unwrap() } @@ -62,6 +62,14 @@ impl Default for DummyTransport { #[async_trait] #[allow(unused_variables)] impl TransportT for DummyTransport { + async fn subscribe_game_account<'a>(&'a self, addr: &'a str) -> Result> + Send + 'a>>> { + let mut states = self.states.lock().unwrap().clone(); + Ok(Box::pin(stream! { + let game_account = states.remove(0); + yield Some(game_account); + })) + } + async fn create_game_account(&self, _params: CreateGameAccountParams) -> Result { Ok(Self::default_game_addr()) } @@ -123,7 +131,7 @@ impl TransportT for DummyTransport { Ok("".into()) } - async fn settle_game(&self, mut params: SettleParams) -> Result<()> { + async fn settle_game(&self, mut params: SettleParams) -> Result { let mut fail_next_settle = self.fail_next_settle.lock().unwrap(); if *fail_next_settle { *fail_next_settle = false; @@ -131,7 +139,7 @@ impl TransportT for DummyTransport { } else if params.addr.eq("TEST") { let mut settles = self.settles.lock().unwrap(); settles.append(&mut params.settles); - Ok(()) + Ok("".into()) } else { Err(Error::GameAccountNotFound) } @@ -185,7 +193,7 @@ impl TransportT for DummyTransport { #[cfg(test)] mod tests { - use race_core::types::Settle; + use race_core::checkpoint::CheckpointOnChain; use crate::prelude::{test_game_addr, TestClient, TestGameAccountBuilder}; @@ -203,16 +211,16 @@ mod tests { #[tokio::test] async fn test_get_state() -> anyhow::Result<()> { let transport = DummyTransport::default(); - let alice = TestClient::player("alice"); - let bob = TestClient::player("bob"); + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); let ga_0 = TestGameAccountBuilder::default().build(); let ga_1 = TestGameAccountBuilder::default() - .add_player(&alice, 100) + .add_player(&mut alice, 100) .build(); let ga_2 = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .add_player(&bob, 100) + .add_player(&mut alice, 100) + .add_player(&mut bob, 100) .build(); let states = vec![ga_0.clone(), ga_1.clone(), ga_2.clone()]; @@ -229,12 +237,12 @@ mod tests { #[tokio::test] async fn test_settle() { let transport = DummyTransport::default(); - let settles = vec![Settle::add("Alice", 100), Settle::add("Bob", 100)]; + let settles = vec![SettleWithAddr::add("Alice", 100), SettleWithAddr::add("Bob", 100)]; let params = SettleParams { addr: test_game_addr(), settles: settles.clone(), transfers: vec![], - checkpoint: vec![], + checkpoint: CheckpointOnChain::default(), settle_version: 0, next_settle_version: 1, }; @@ -256,7 +264,7 @@ mod tests { addr: test_game_addr(), settles: vec![], transfers: vec![], - checkpoint: vec![], + checkpoint: CheckpointOnChain::default(), settle_version: 0, next_settle_version: 1, }; diff --git a/transactor/Cargo.toml b/transactor/Cargo.toml index 89441b8b..66d3a9be 100644 --- a/transactor/Cargo.toml +++ b/transactor/Cargo.toml @@ -22,6 +22,8 @@ race-transport = { workspace = true } race-encryptor = { workspace = true } race-client = { workspace = true } race-env = { workspace = true } +race-local-db = { workspace = true } +tracing-appender = { workspace = true } uuid = { workspace = true, features = ["v4", "fast-rng"] } tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true, features = ["sync"] } @@ -43,6 +45,7 @@ async-stream = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } base64 = { workspace = true } +sha256 = { workspace = true } [dev-dependencies] race-test = { path = "../test" } diff --git a/transactor/src/component.rs b/transactor/src/component.rs index fceb2efb..28276134 100644 --- a/transactor/src/component.rs +++ b/transactor/src/component.rs @@ -10,11 +10,14 @@ mod voter; mod wrapped_client; mod wrapped_handler; mod wrapped_transport; +mod wrapped_storage; +mod event_bridge; pub use event_bus::CloseReason; pub use broadcaster::Broadcaster; pub use common::Component; pub use common::PortsHandle; +pub use common::ComponentEnv; pub use connection::{LocalConnection, RemoteConnection}; pub use event_bus::EventBus; pub use event_loop::EventLoop; @@ -25,3 +28,5 @@ pub use voter::Voter; pub use wrapped_client::WrappedClient; pub use wrapped_handler::WrappedHandler; pub use wrapped_transport::WrappedTransport; +pub use wrapped_storage::WrappedStorage; +pub use event_bridge::{EventBridgeChild, EventBridgeParent}; diff --git a/transactor/src/component/broadcaster.rs b/transactor/src/component/broadcaster.rs index 8b564215..092ec426 100644 --- a/transactor/src/component/broadcaster.rs +++ b/transactor/src/component/broadcaster.rs @@ -6,14 +6,15 @@ use std::sync::Arc; use async_trait::async_trait; use race_api::event::Event; -use race_core::types::{BroadcastFrame, GameAccount, TxState}; +use race_core::checkpoint::{Checkpoint, CheckpointOffChain}; +use race_core::types::{BroadcastFrame, BroadcastSync, EventHistory, TxState}; use tokio::sync::{broadcast, Mutex}; -use tracing::{debug, error}; +use tracing::{debug, error, info}; use crate::component::common::{Component, ConsumerPorts}; use crate::frame::EventFrame; -use super::CloseReason; +use super::{CloseReason, ComponentEnv}; /// Backup events in memeory, for new connected clients. The /// `settle_version` and `access_version` are the values at the time @@ -25,48 +26,51 @@ pub struct EventBackup { pub timestamp: u64, pub access_version: u64, pub settle_version: u64, -} - -#[derive(Debug)] -pub struct Checkpoint { - pub access_version: u64, - pub settle_version: u64, + pub state_sha: String, } #[derive(Debug)] pub struct EventBackupGroup { + pub state_sha: String, + pub sync: BroadcastSync, pub events: LinkedList, - pub checkpoint: Checkpoint, pub settle_version: u64, pub access_version: u64, + pub checkpoint: Option, } pub struct BroadcasterContext { - game_addr: String, + id: String, event_backup_groups: Arc>>, broadcast_tx: broadcast::Sender, + #[allow(unused)] + debug_mode: bool, } /// A component that pushes event to clients. pub struct Broadcaster { - game_addr: String, + id: String, + #[allow(unused)] + game_id: usize, event_backup_groups: Arc>>, broadcast_tx: broadcast::Sender, } impl Broadcaster { - pub fn init(game_account: &GameAccount) -> (Self, BroadcasterContext) { + pub fn init(id: String, game_id: usize, debug_mode: bool) -> (Self, BroadcasterContext) { let event_backup_groups = Arc::new(Mutex::new(LinkedList::new())); let (broadcast_tx, broadcast_rx) = broadcast::channel(10); drop(broadcast_rx); ( Self { - game_addr: game_account.addr.clone(), + id: id.clone(), + game_id, event_backup_groups: event_backup_groups.clone(), broadcast_tx: broadcast_tx.clone(), }, BroadcasterContext { - game_addr: game_account.addr.clone(), + id, + debug_mode, event_backup_groups, broadcast_tx, }, @@ -77,88 +81,142 @@ impl Broadcaster { self.broadcast_tx.subscribe() } - pub async fn retrieve_histories(&self, settle_version: u64) -> Vec { + pub async fn get_checkpoint(&self, settle_version: u64) -> Option { + let event_backup_groups = self.event_backup_groups.lock().await; + info!("Get checkpoint with settle_version = {}", settle_version); + + for group in event_backup_groups.iter() { + if group.settle_version == settle_version { + info!("Found checkpoint"); + return group.checkpoint.as_ref().map(|cp| cp.derive_offchain_part()); + } + } + info!("Found checkpoint"); + None + } - let mut histories: Vec = Vec::new(); + pub async fn retrieve_histories(&self, settle_version: u64) -> Vec { + // info!( + // "{} Retrieve the histories, settle_version: {}", + // Self::name(), + // settle_version + // ); + + let mut frames: Vec = Vec::new(); + let event_backup_groups = self.event_backup_groups.lock().await; for group in event_backup_groups.iter() { + let mut histories: Vec = Vec::new(); if group.settle_version >= settle_version { + info!("Broadcast sync {:?}", group.sync); + frames.push(BroadcastFrame::Sync { + sync: group.sync.clone(), + }); for event in group.events.iter() { - histories.push(BroadcastFrame::Event { - game_addr: self.game_addr.clone(), + histories.push(EventHistory { event: event.event.clone(), timestamp: event.timestamp, - }) + state_sha: event.state_sha.clone(), + }); } + frames.push(BroadcastFrame::EventHistories { + game_addr: self.id.clone(), + checkpoint_off_chain: group.checkpoint.as_ref().map(|c| c.derive_offchain_part()), + histories, + state_sha: group.state_sha.clone(), + }) } } - histories + frames } } #[async_trait] impl Component for Broadcaster { - fn name(&self) -> &str { + fn name() -> &'static str { "Broadcaster" } - async fn run(mut ports: ConsumerPorts, ctx: BroadcasterContext) -> CloseReason { + async fn run( + mut ports: ConsumerPorts, + ctx: BroadcasterContext, + env: ComponentEnv, + ) -> CloseReason { while let Some(event) = ports.recv().await { match event { EventFrame::SendMessage { message } => { let r = ctx.broadcast_tx.send(BroadcastFrame::Message { - game_addr: ctx.game_addr.clone(), + game_addr: ctx.id.clone(), message, }); if let Err(e) = r { // Usually it means no receivers - debug!("Failed to broadcast event: {:?}", e); + debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); } } + EventFrame::Checkpoint { access_version, settle_version, + checkpoint, + state_sha, + .. } => { + info!( + "{} Create new history group (via Checkpoint) with access_version = {}, settle_version = {}", + env.log_prefix, access_version, settle_version + ); let mut event_backup_groups = ctx.event_backup_groups.lock().await; - let checkpoint = Checkpoint { + event_backup_groups.push_back(EventBackupGroup { + sync: BroadcastSync::new(access_version), + state_sha, + events: LinkedList::new(), access_version, settle_version, - }; + checkpoint: Some(checkpoint), + }); + } + EventFrame::InitState { access_version, settle_version, .. } => { + info!( + "{} Create new history group (via InitState) with access_version = {}, settle_version = {}", + env.log_prefix, access_version, settle_version + ); + let mut event_backup_groups = ctx.event_backup_groups.lock().await; event_backup_groups.push_back(EventBackupGroup { + sync: BroadcastSync::new(access_version), events: LinkedList::new(), - checkpoint, access_version, settle_version, + checkpoint: None, + state_sha: "".into(), }); + } EventFrame::TxState { tx_state } => match tx_state { - TxState::PlayerConfirming { - confirm_players, - access_version, - } => { - let tx_state = TxState::PlayerConfirming { - confirm_players, - access_version, - }; + TxState::SettleSucceed { .. } => { + let r = ctx.broadcast_tx.send(BroadcastFrame::TxState { tx_state }); + if let Err(e) = r { + debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); + } + } + TxState::PlayerConfirming { .. } => { let r = ctx.broadcast_tx.send(BroadcastFrame::TxState { tx_state }); if let Err(e) = r { - debug!("Failed to broadcast event: {:?}", e); + debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); } } - TxState::PlayerConfirmingFailed(access_version) => { - let r = ctx.broadcast_tx.send(BroadcastFrame::TxState { - tx_state: TxState::PlayerConfirmingFailed(access_version), - }); + TxState::PlayerConfirmingFailed(_) => { + let r = ctx.broadcast_tx.send(BroadcastFrame::TxState { tx_state }); if let Err(e) = r { - debug!("Failed to broadcast event: {:?}", e); + debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); } } }, @@ -167,8 +225,10 @@ impl Component for Broadcaster { access_version, settle_version, timestamp, + state_sha, + .. } => { - debug!("Broadcaster receive event: {}", event); + // info!("{} Broadcaster receive event: {}", env.log_prefix, event); let mut event_backup_groups = ctx.event_backup_groups.lock().await; if let Some(current) = event_backup_groups.back_mut() { @@ -177,24 +237,59 @@ impl Component for Broadcaster { settle_version, access_version, timestamp, + state_sha: state_sha.clone(), }); } else { - error!("Received event without checkpoint"); + error!("{} Received event without checkpoint", env.log_prefix); } // Keep 10 groups at most if event_backup_groups.len() > 10 { event_backup_groups.pop_front(); } + drop(event_backup_groups); let r = ctx.broadcast_tx.send(BroadcastFrame::Event { - game_addr: ctx.game_addr.clone(), + game_addr: ctx.id.clone(), event, timestamp, + state_sha, }); if let Err(e) = r { // Usually it means no receivers - debug!("Failed to broadcast event: {:?}", e); + debug!("{} Failed to broadcast event: {:?}", env.log_prefix, e); + } + } + EventFrame::Sync { + new_servers, + new_players, + access_version, + transactor_addr, + } => { + let sync = BroadcastSync { + new_players, + new_servers, + access_version, + transactor_addr, + }; + + let mut event_backup_groups = ctx.event_backup_groups.lock().await; + if let Some(current) = event_backup_groups.back_mut() { + info!("{} Merge sync: {:?}", env.log_prefix, sync); + current.sync.merge(&sync); + } else { + error!("{} Sync dropped", env.log_prefix); + } + drop(event_backup_groups); + + let broadcast_frame = BroadcastFrame::Sync { sync }; + let r = ctx.broadcast_tx.send(broadcast_frame); + + if let Err(e) = r { + debug!( + "{} Failed to broadcast node updates: {:?}", + env.log_prefix, e + ); } } EventFrame::Shutdown => { @@ -216,14 +311,15 @@ mod tests { #[tokio::test] async fn test_broadcast_event() { - let alice = TestClient::player("alice"); - let bob = TestClient::player("bob"); + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); let game_account = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .add_player(&bob, 100) + .add_player(&mut alice, 100) + .add_player(&mut bob, 100) .build(); - let (broadcaster, ctx) = Broadcaster::init(&game_account); - let handle = broadcaster.start(ctx); + + let (broadcaster, ctx) = Broadcaster::init(game_account.addr.clone(), 0, true); + let handle = broadcaster.start("", ctx); let mut rx = broadcaster.get_broadcast_rx(); // BroadcastFrame::Event @@ -233,18 +329,20 @@ mod tests { settle_version: 10, timestamp: 0, event: Event::Custom { - sender: "Alice".into(), + sender: alice.id(), raw: "CUSTOM EVENT".into(), }, + state_sha: "".into(), }; let broadcast_frame = BroadcastFrame::Event { game_addr: game_account.addr, timestamp: 0, event: Event::Custom { - sender: "Alice".into(), + sender: alice.id(), raw: "CUSTOM EVENT".into(), }, + state_sha: "".into(), }; handle.send_unchecked(event_frame).await; @@ -261,7 +359,8 @@ mod tests { balance: 100, access_version: 10, verify_key: "alice".into(), - }], + } + .into()], access_version: 10, }; let event_frame = EventFrame::TxState { diff --git a/transactor/src/component/common.rs b/transactor/src/component/common.rs index 573908bc..a0b1dee5 100644 --- a/transactor/src/component/common.rs +++ b/transactor/src/component/common.rs @@ -5,12 +5,14 @@ use tokio::{ }; use tracing::{info, warn}; -use crate::frame::EventFrame; +use crate::{frame::EventFrame, utils::addr_shorthand}; -use super::event_bus::CloseReason; +use super::{event_bus::CloseReason}; /// An interface for a component that can be attached to the event bus. pub trait Attachable { + fn id(&self) -> &str; + /// Return the input channel of current component. /// Return `None` when the component does not accept input. fn input(&mut self) -> Option>; @@ -28,14 +30,16 @@ pub struct PortsHandleInner { } pub struct PortsHandle { + pub id: String, input_tx: Option>, output_rx: Option>, join_handle: JoinHandle, } impl PortsHandle { - fn from_inner(value: PortsHandleInner, join_handle: JoinHandle) -> Self { + fn from_inner>(id: S, value: PortsHandleInner, join_handle: JoinHandle) -> Self { Self { + id: id.into(), input_tx: value.input_tx, output_rx: value.output_rx, join_handle, @@ -68,6 +72,10 @@ impl PortsHandle { } impl Attachable for PortsHandle { + fn id(&self) -> &str { + self.id.as_str() + } + fn input(&mut self) -> Option> { if self.input_tx.is_some() { self.input_tx.clone() @@ -125,6 +133,7 @@ impl ProducerPorts { self.tx.send(frame).await } + #[allow(dead_code)] pub fn is_tx_closed(&self) -> bool { self.tx.is_closed() } @@ -170,6 +179,12 @@ impl PipelinePorts { self.tx.send(frame).await } + pub fn clone_as_producer(&self) -> ProducerPorts { + ProducerPorts { + tx: self.tx.clone() + } + } + pub async fn send(&self, frame: EventFrame) { match self.tx.send(frame).await { Ok(_) => (), @@ -200,19 +215,31 @@ impl Ports for PipelinePorts { } } +pub struct ComponentEnv { + pub log_prefix: String, +} + +impl ComponentEnv { + pub fn new(addr: &str, component_name: &str) -> Self { + let addr_short = addr_shorthand(addr); + Self { log_prefix: format!("[{}|{}]", addr_short, component_name)} + } +} + #[async_trait] pub trait Component where P: Ports + 'static, C: Send + 'static, { - fn name(&self) -> &str; + fn name() -> &'static str; - fn start(&self, context: C) -> PortsHandle { - info!("Starting component: {}", self.name()); + fn start(&self, addr: &str, context: C) -> PortsHandle { + info!("Starting component: {}", Self::name()); let (ports, ports_handle_inner) = P::create(); - let join_handle = tokio::spawn(async move { Self::run(ports, context).await }); - PortsHandle::from_inner(ports_handle_inner, join_handle) + let env = ComponentEnv::new(addr, Self::name()); + let join_handle = tokio::spawn(async move { Self::run(ports, context, env).await }); + PortsHandle::from_inner(Self::name(), ports_handle_inner, join_handle) } - async fn run(ports: P, context: C) -> CloseReason; + async fn run(ports: P, context: C, env: ComponentEnv) -> CloseReason; } diff --git a/transactor/src/component/connection.rs b/transactor/src/component/connection.rs index d62e898f..469e7618 100644 --- a/transactor/src/component/connection.rs +++ b/transactor/src/component/connection.rs @@ -20,17 +20,15 @@ use jsonrpsee::{ rpc_params, ws_client::{WsClient, WsClientBuilder}, }; +use race_api::error::{Error, Result}; +use race_core::types::{BroadcastFrame, SubscribeEventParams}; use race_core::{ connection::ConnectionT, encryptor::EncryptorT, types::{AttachGameParams, ExitGameParams, SubmitEventParams}, }; -use race_api::error::{Error, Result}; -use race_core::{ - types::{BroadcastFrame, SubscribeEventParams}, -}; -use crate::frame::EventFrame; +use crate::{frame::EventFrame, utils::current_timestamp}; use crate::utils::base64_decode; use crate::{component::common::Attachable, utils::base64_encode}; @@ -52,6 +50,7 @@ impl ConnectionT for LocalConnection { self.output_tx .send(EventFrame::SendEvent { event: params.event, + timestamp: current_timestamp(), }) .await .map_err(|e| Error::InternalError(e.to_string())) @@ -74,6 +73,10 @@ impl LocalConnection { } impl Attachable for LocalConnection { + fn id(&self) -> &str { + "LocalConnection" + } + fn input(&mut self) -> Option> { None } @@ -135,21 +138,15 @@ impl RemoteConnection { }) } - // async fn request(&self, game_addr: &str, method: &str) { - // let mut rpc_client = self.rpc_client.lock().await; - // let mut retries = 0; - - // } - fn make_request

(&self, game_addr: &str, params: &P) -> Result where P: BorshSerialize, { - let params_bytes = params.try_to_vec()?; + let params_bytes = borsh::to_vec(¶ms)?; let sig = self .encryptor .sign(¶ms_bytes, self.server_addr.clone())?; - let sig_bytes = sig.try_to_vec()?; + let sig_bytes = borsh::to_vec(&sig)?; let p = base64_encode(¶ms_bytes); let s = base64_encode(&sig_bytes); Ok(rpc_params![game_addr, p, s]) @@ -159,7 +156,7 @@ impl RemoteConnection { where P: BorshSerialize, { - let params_bytes = params.try_to_vec()?; + let params_bytes = borsh::to_vec(¶ms)?; let p = base64_encode(¶ms_bytes); Ok(rpc_params![game_addr, p]) } @@ -185,10 +182,12 @@ impl RemoteConnection { Ok(ret) => return Ok(ret), Err(RestartNeeded(e)) => { // For reconnecting - warn!("Try reconnect due to error: {:?}", e); + warn!("Try reconnect due to error[{}]: {:?}", method, e); *rpc_client = None; } - Err(_) => (), + Err(e) => { + warn!("Error in request[{}]: {:?}", method, e); + }, } if retries < self.max_retries { diff --git a/transactor/src/component/event_bridge.rs b/transactor/src/component/event_bridge.rs new file mode 100644 index 00000000..df2a90c9 --- /dev/null +++ b/transactor/src/component/event_bridge.rs @@ -0,0 +1,255 @@ +use crate::frame::{EventFrame, SignalFrame}; +use async_trait::async_trait; +use tokio::sync::{broadcast, mpsc}; +use tracing::{info, log::error}; + +use super::{common::PipelinePorts, CloseReason, Component, ComponentEnv}; + +#[allow(dead_code)] +pub struct EventBridgeParentContext { + tx: broadcast::Sender, + rx: mpsc::Receiver, + signal_tx: mpsc::Sender, +} + +#[derive(Clone, Debug)] +pub struct EventBridgeParent { + tx: mpsc::Sender, + bc: broadcast::Sender, +} + +pub struct EventBridgeChildContext { + pub game_id: usize, + tx: mpsc::Sender, + rx: broadcast::Receiver, +} + +pub struct EventBridgeChild { + pub game_id: usize, +} + +impl EventBridgeParent { + pub fn init(signal_tx: mpsc::Sender) -> (Self, EventBridgeParentContext) { + let (mpsc_tx, mpsc_rx) = mpsc::channel(10); + let (bc_tx, _bc_rx) = broadcast::channel(10); + ( + Self { + tx: mpsc_tx, + bc: bc_tx.clone(), + }, + EventBridgeParentContext { + tx: bc_tx, + rx: mpsc_rx, + signal_tx, + }, + ) + } + + pub fn derive_child(&self, game_id: usize) -> (EventBridgeChild, EventBridgeChildContext) { + ( + EventBridgeChild { + game_id: game_id.clone(), + }, + EventBridgeChildContext { + game_id, + tx: self.tx.clone(), + rx: self.bc.subscribe(), + }, + ) + } +} + +impl EventBridgeParent { + /// Read event from both the local event bus and the bridge. + /// Return (true, event) when the event is from the bridge. + /// Return None when bridge is closed. + async fn read_event( + ports: &mut PipelinePorts, + rx: &mut mpsc::Receiver, + ) -> Option<(bool, EventFrame)> { + tokio::select! { + e = rx.recv() => { + if let Some(e) = e { + Some((true, e)) + } else { + None + } + }, + e = ports.recv() => { + if let Some(e) = e { + Some((false, e)) + } else { + None + } + }, + } + } +} + +#[async_trait] +impl Component for EventBridgeParent { + fn name() -> &'static str { + "Event Bridge (Parent)" + } + + async fn run( + mut ports: PipelinePorts, + mut ctx: EventBridgeParentContext, + env: ComponentEnv, + ) -> CloseReason { + while let Some((from_bridge, event_frame)) = Self::read_event(&mut ports, &mut ctx.rx).await + { + if from_bridge { + match event_frame { + EventFrame::SendBridgeEvent { + from, + dest, + event, + access_version, + settle_version, + checkpoint, + } => { + info!("{} Receives event: {}", env.log_prefix, event); + ports + .send(EventFrame::RecvBridgeEvent { + from, + dest, + event, + access_version, + settle_version, + checkpoint, + }) + .await; + } + _ => (), + } + } else { + match event_frame { + EventFrame::LaunchSubGame { spec } => { + let f = SignalFrame::LaunchSubGame { spec: *spec }; + if let Err(e) = ctx.signal_tx.send(f).await { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + EventFrame::Shutdown => { + if !ctx.tx.is_empty() { + info!("{} Sends Shutdown", env.log_prefix); + if let Err(e) = ctx.tx.send(event_frame) { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + break; + } + EventFrame::SendBridgeEvent { dest, .. } if dest != 0 => { + info!("{} Sends event: {}", env.log_prefix, event_frame); + if let Err(e) = ctx.tx.send(event_frame) { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + EventFrame::Sync { .. } => { + if !ctx.tx.is_empty() { + info!("{} Sends event: {}", env.log_prefix, event_frame); + if let Err(e) = ctx.tx.send(event_frame) { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + } + _ => continue, + } + } + } + + CloseReason::Complete + } +} + +impl EventBridgeChild { + /// Read event from both the local event bus and the bridge. + /// Return (true, event) when the event is from the bridge. + /// Return None when bridge is closed. + async fn read_event( + ports: &mut PipelinePorts, + rx: &mut broadcast::Receiver, + ) -> Option<(bool, EventFrame)> { + tokio::select! { + e = rx.recv() => { + if let Ok(e) = e { + Some((true, e)) + } else { + None + } + }, + e = ports.recv() => { + if let Some(e) = e { + Some((false, e)) + } else { + None + } + } + } + } +} + +#[async_trait] +impl Component for EventBridgeChild { + fn name() -> &'static str { + "Event Bridge (Child)" + } + + async fn run( + mut ports: PipelinePorts, + mut ctx: EventBridgeChildContext, + env: ComponentEnv, + ) -> CloseReason { + while let Some((from_bridge, event_frame)) = Self::read_event(&mut ports, &mut ctx.rx).await + { + if from_bridge { + match event_frame { + EventFrame::Shutdown => { + info!("{} Receives Shutdown, quit", env.log_prefix); + ports.send(event_frame).await; + break; + } + EventFrame::Sync { .. } => { + info!("{} Receives event: {}", env.log_prefix, event_frame); + ports.send(event_frame).await; + } + EventFrame::SendBridgeEvent { + from, + dest, + event, + access_version, + settle_version, + checkpoint, + } if dest == ctx.game_id => { + info!("{} Receives event: {}", env.log_prefix, event); + ports + .send(EventFrame::RecvBridgeEvent { + from, + dest, + event, + access_version, + settle_version, + checkpoint, + }) + .await; + } + _ => {} + } + } else { + match event_frame { + EventFrame::Shutdown => break, + EventFrame::SendBridgeEvent { dest, .. } if dest != ctx.game_id => { + info!("{} Sends event: {}", env.log_prefix, event_frame); + if let Err(e) = ctx.tx.send(event_frame).await { + error!("{} Failed to send: {}", env.log_prefix, e); + } + } + _ => continue, + } + } + } + + CloseReason::Complete + } +} diff --git a/transactor/src/component/event_bus.rs b/transactor/src/component/event_bus.rs index 4cdb442d..f5f25c12 100644 --- a/transactor/src/component/event_bus.rs +++ b/transactor/src/component/event_bus.rs @@ -6,15 +6,52 @@ use tracing::{error, warn}; use crate::component::common::Attachable; use crate::frame::EventFrame; +use crate::utils::addr_shorthand; /// An event bus that passes the events between different components. pub struct EventBus { + #[allow(unused)] + addr: String, tx: mpsc::Sender, - attached_txs: Arc>>>, + attached_txs: Arc)>>>, close_rx: watch::Receiver, } impl EventBus { + pub fn new(addr: String) -> Self { + let (close_tx, close_rx) = watch::channel(false); + let (tx, mut rx) = mpsc::channel::(32); + let txs: Arc)>>> = Arc::new(Mutex::new(vec![])); + let attached_txs = txs.clone(); + let addr_1 = addr_shorthand(&addr); + + tokio::spawn(async move { + while let Some(msg) = rx.recv().await { + let txs = attached_txs.lock().await; + for (id, t) in txs.iter() { + if t.send(msg.clone()).await.is_err() { + warn!( + "[{}] Failed to send message: {} to component: {}", + addr_1, + msg, + id + ); + } + } + if matches!(msg, EventFrame::Shutdown) { + close_tx.send(true).unwrap(); + break; + } + } + }); + Self { + addr, + tx, + attached_txs: txs, + close_rx, + } + } + pub async fn attach(&self, attachable: &mut T) where T: Attachable, @@ -50,7 +87,7 @@ impl EventBus { if let Some(tx) = attachable.input() { let mut txs = self.attached_txs.lock().await; - txs.push(tx.clone()); + txs.push((attachable.id().to_string(), tx.clone())); } } @@ -64,30 +101,7 @@ impl EventBus { impl Default for EventBus { fn default() -> Self { - let (close_tx, close_rx) = watch::channel(false); - let (tx, mut rx) = mpsc::channel::(32); - let txs: Arc>>> = Arc::new(Mutex::new(vec![])); - let attached_txs = txs.clone(); - - tokio::spawn(async move { - while let Some(msg) = rx.recv().await { - let txs = attached_txs.lock().await; - for t in txs.iter() { - if t.send(msg.clone()).await.is_err() { - warn!("Failed to send message"); - } - } - if matches!(msg, EventFrame::Shutdown) { - close_tx.send(true).unwrap(); - break; - } - } - }); - Self { - tx, - attached_txs: txs, - close_rx, - } + Self::new("".to_string()) } } @@ -101,7 +115,7 @@ pub enum CloseReason { #[cfg(test)] mod tests { - use crate::component::common::{Component, ConsumerPorts, ProducerPorts}; + use crate::component::{common::{Component, ConsumerPorts, ProducerPorts}, ComponentEnv}; use super::*; use async_trait::async_trait; @@ -115,11 +129,11 @@ mod tests { #[async_trait] impl Component for TestProducer { - fn name(&self) -> &str { + fn name() -> &'static str { "Test Producer" } - async fn run(ports: ProducerPorts, _ctx: TestProducerCtx) -> CloseReason { + async fn run(ports: ProducerPorts, _ctx: TestProducerCtx, _env: ComponentEnv) -> CloseReason { loop { println!("Producer started"); let event = EventFrame::Sync { @@ -158,11 +172,11 @@ mod tests { #[async_trait] impl Component for TestConsumer { - fn name(&self) -> &str { + fn name() -> &'static str { "Test Consumer" } - async fn run(mut ports: ConsumerPorts, ctx: TestConsumerCtx) -> CloseReason { + async fn run(mut ports: ConsumerPorts, ctx: TestConsumerCtx, _env: ComponentEnv) -> CloseReason { println!("Consumer started"); loop { match ports.recv().await { @@ -193,8 +207,8 @@ mod tests { let (c, c_ctx) = TestConsumer::init(); let eb = EventBus::default(); - let mut p_handle = p.start(p_ctx); - let mut c_handle = c.start(c_ctx); + let mut p_handle = p.start("producer", p_ctx,); + let mut c_handle = c.start("consumer", c_ctx); eb.attach(&mut p_handle).await; eb.attach(&mut c_handle).await; diff --git a/transactor/src/component/event_loop.rs b/transactor/src/component/event_loop.rs index 6aa6879a..73b03c6e 100644 --- a/transactor/src/component/event_loop.rs +++ b/transactor/src/component/event_loop.rs @@ -1,4 +1,6 @@ -use std::time::{Duration, UNIX_EPOCH}; +use race_api::effect::SubGame; +use sha256::digest; +use std::time::Duration; use async_trait::async_trait; use race_api::error::Error; @@ -11,8 +13,10 @@ use crate::component::common::{Component, PipelinePorts}; use crate::component::event_bus::CloseReason; use crate::component::wrapped_handler::WrappedHandler; use crate::frame::EventFrame; -use crate::utils::addr_shorthand; -use race_core::types::{ClientMode, GameAccount}; +use race_core::types::{ClientMode, GameAccount, GameMode, GamePlayer, SubGameSpec}; +use crate::utils::current_timestamp; + +use super::ComponentEnv; fn log_execution_context(ctx: &GameContext, evt: &Event) { info!("Execution context"); @@ -26,7 +30,8 @@ fn log_execution_context(ctx: &GameContext, evt: &Event) { pub struct EventLoopContext { handler: WrappedHandler, game_context: GameContext, - mode: ClientMode, + client_mode: ClientMode, + game_mode: GameMode, } pub trait WrappedGameHandler: Send { @@ -37,72 +42,132 @@ pub trait WrappedGameHandler: Send { pub struct EventLoop {} -async fn handle( +async fn handle_event( handler: &mut WrappedHandler, game_context: &mut GameContext, event: Event, ports: &PipelinePorts, - - #[allow(unused)] mode: ClientMode, + client_mode: ClientMode, + game_mode: GameMode, + timestamp: u64, + env: &ComponentEnv, ) -> Option { info!( - "{} Handle event: {}", - addr_shorthand(game_context.get_game_addr()), - event + "{} Handle event: {}, timestamp: {}", + env.log_prefix, + event, + timestamp ); - let access_version = game_context.get_access_version(); - let settle_version = game_context.get_settle_version(); + game_context.set_timestamp(timestamp); + let access_version = game_context.access_version(); + let settle_version = game_context.settle_version(); match handler.handle_event(game_context, &event) { Ok(effects) => { - ports - .send(EventFrame::Broadcast { - event, - access_version, - settle_version, - timestamp: game_context.get_timestamp(), - }) - .await; + let state = game_context.get_handler_state_raw().to_owned(); + let state_sha = digest(&state); + // info!("{} Game state SHA: {}", env.log_prefix, state_sha); - if game_context.is_checkpoint() { + // Broacast the event to clients + if client_mode == ClientMode::Transactor { ports - .send(EventFrame::Checkpoint { - access_version: game_context.get_access_version(), - settle_version: game_context.get_settle_version(), + .send(EventFrame::Broadcast { + event, + access_version, + settle_version, + timestamp, + state_sha: state_sha.clone(), }) .await; } + // Update the local client ports .send(EventFrame::ContextUpdated { context: Box::new(game_context.clone()), }) .await; - // We do optimistic updates here - if let Some(effects) = effects { + // Start game + if client_mode == ClientMode::Transactor && effects.start_game { + ports + .send(EventFrame::GameStart { + access_version: game_context.access_version(), + }) + .await; + } + + // Send the settlement when there's one + if let Some(checkpoint) = effects.checkpoint { + let checkpoint_size = checkpoint.get_data(game_context.game_id()).map(|d| d.len()); info!( - "{} Send settlements: {:?}", - addr_shorthand(game_context.get_game_addr()), - effects + "{} Create checkpoint, settle_version: {}, size: {:?}, state_sha: {}", + env.log_prefix, + game_context.settle_version(), + checkpoint_size, + state_sha ); + ports - .send(EventFrame::Settle { + .send(EventFrame::Checkpoint { + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), + previous_settle_version: settle_version, + checkpoint, settles: effects.settles, transfers: effects.transfers, - checkpoint: effects.checkpoint, - settle_version, + state_sha: state_sha.clone(), }) .await; } + + // Launch sub games + if game_mode == GameMode::Main { + for sub_game in effects.launch_sub_games { + info!("{} Launch sub game: {}", env.log_prefix, sub_game.id); + let cp = game_context.checkpoint_mut(); + let SubGame {bundle_addr, id, mut init_account} = sub_game; + // Use the existing checkpoint when possible. + init_account.checkpoint = cp.get_data(id); + + let ef = EventFrame::LaunchSubGame { + spec: Box::new(SubGameSpec { + game_addr: game_context.game_addr().to_owned(), + game_id: id, + bundle_addr, + nodes: game_context.get_nodes().into(), + access_version: game_context.access_version(), + settle_version, + init_account, + }), + }; + ports.send(ef).await; + } + } + + // Emit bridge events + if client_mode == ClientMode::Transactor { + for be in effects.bridge_events { + info!("{} Send bridge event, dest: {}, checkpoint sha: {}", env.log_prefix, be.dest, state_sha); + let ef = EventFrame::SendBridgeEvent { + from: game_context.game_id(), + dest: be.dest, + event: Event::Bridge { + dest: be.dest, + raw: be.raw, + join_players: be.join_players, + }, + access_version: game_context.access_version(), + settle_version: game_context.settle_version(), + checkpoint: state.clone(), + }; + ports.send(ef).await; + } + } } Err(e) => { - warn!( - "{} Handle event error: {}", - addr_shorthand(game_context.get_game_addr()), - e.to_string() - ); + warn!("{} Handle event error: {}", env.log_prefix, e.to_string()); log_execution_context(game_context, &event); match e { Error::WasmExecutionError(_) | Error::WasmMemoryOverflow => { @@ -118,7 +183,7 @@ async fn handle( /// Take the event from clients or the pending dispatched event. /// Transactor will retrieve events from both dispatching event and /// ports, while Validator will retrieve events from only ports. -async fn retrieve_event( +async fn read_event( ports: &mut PipelinePorts, game_context: &mut GameContext, mode: ClientMode, @@ -131,7 +196,7 @@ async fn retrieve_event( if dispatch.timeout <= timestamp { let event = dispatch.event.clone(); game_context.cancel_dispatch(); - return Some(EventFrame::SendServerEvent { event }); + return Some(EventFrame::SendServerEvent { event, timestamp }); } let to = tokio::time::sleep(Duration::from_millis(dispatch.timeout - timestamp)); select! { @@ -141,7 +206,7 @@ async fn retrieve_event( _ = to => { let event = dispatch.event.clone(); game_context.cancel_dispatch(); - Some(EventFrame::SendServerEvent { event }) + Some(EventFrame::SendServerEvent { event, timestamp }) } } } else { @@ -151,104 +216,232 @@ async fn retrieve_event( #[async_trait] impl Component for EventLoop { - fn name(&self) -> &str { + fn name() -> &'static str { "Event Loop" } - async fn run(mut ports: PipelinePorts, ctx: EventLoopContext) -> CloseReason { + async fn run( + mut ports: PipelinePorts, + ctx: EventLoopContext, + env: ComponentEnv, + ) -> CloseReason { let mut handler = ctx.handler; let mut game_context = ctx.game_context; // Read games from event bus - while let Some(event_frame) = retrieve_event(&mut ports, &mut game_context, ctx.mode).await + while let Some(event_frame) = + read_event(&mut ports, &mut game_context, ctx.client_mode).await { - // Set timestamp to current time - // Reset some disposable states. - game_context.prepare_for_next_event(current_timestamp()); - match event_frame { - EventFrame::InitState { init_account } => { - info!("Servers: {:?}", game_context.get_servers()); - if let Err(e) = game_context - .apply_checkpoint(init_account.access_version, init_account.settle_version) - { - error!("Failed to apply checkpoint: {:?}", e); + EventFrame::InitState { + init_account, + access_version, + settle_version, + } => { + if let Err(e) = game_context.apply_checkpoint(access_version, settle_version) { + error!("{} Failed to apply checkpoint: {:?}, context settle version: {}, init account settle version: {}", env.log_prefix, e, + game_context.settle_version(), settle_version); ports.send(EventFrame::Shutdown).await; return CloseReason::Fault(e); } if let Err(e) = handler.init_state(&mut game_context, &init_account) { - error!("Failed to initialize state: {:?}", e); + error!("{} Failed to initialize state: {:?}", env.log_prefix, e); ports.send(EventFrame::Shutdown).await; return CloseReason::Fault(e); } - info!("Servers: {:?}", game_context.get_servers()); + let state_sha = digest(game_context.get_handler_state_raw()); info!( - "{} Initialize game state, access_version = {}, settle_version = {}", - addr_shorthand(&init_account.addr), - init_account.access_version, - init_account.settle_version + "{} Initialize game state, access_version: {}, settle_version: {}, SHA: {}", + env.log_prefix, access_version, settle_version, state_sha ); game_context.dispatch_safe(Event::Ready, 0); - ports - .send(EventFrame::Checkpoint { - access_version: init_account.access_version, - settle_version: init_account.settle_version, - }) - .await; } + + EventFrame::GameStart { .. } => { + let timestamp = current_timestamp(); + if ctx.client_mode == ClientMode::Transactor { + let event = Event::GameStart; + if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await + { + ports.send(EventFrame::Shutdown).await; + return close_reason; + } + } + } + EventFrame::Sync { new_players, new_servers, access_version, transactor_addr, } => { - let event = Event::Sync { - new_players, - new_servers, - access_version, - transactor_addr, - }; - if let Some(close_reason) = - handle(&mut handler, &mut game_context, event, &ports, ctx.mode).await + let timestamp = current_timestamp(); + + info!( + "{} handle Sync, access_version: {:?}", + env.log_prefix, access_version + ); + game_context.set_access_version(access_version); + + // Add servers to context + for server in new_servers.iter() { + let mode = if server.addr.eq(&transactor_addr) { + ClientMode::Transactor + } else { + ClientMode::Validator + }; + game_context.add_node(server.addr.clone(), server.access_version, mode); + info!( + "{} Game context add server: {:?}", + env.log_prefix, server.addr + ); + } + + let mut new_players_1: Vec = Vec::with_capacity(new_players.len()); + for p in new_players.iter() { + new_players_1.push(p.clone().into()); + game_context.add_node(p.addr.clone(), p.access_version, ClientMode::Player); + } + + // We only generate join event in Transactor & Main mode. + if ctx.client_mode == ClientMode::Transactor + && ctx.game_mode == GameMode::Main + && !new_players.is_empty() { - ports.send(EventFrame::Shutdown).await; - return close_reason; + let event = Event::Join { + players: new_players_1, + }; + if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await + { + ports.send(EventFrame::Shutdown).await; + return close_reason; + } } } EventFrame::PlayerLeaving { player_addr } => { - let event = Event::Leave { player_addr }; - if let Some(close_reason) = - handle(&mut handler, &mut game_context, event, &ports, ctx.mode).await + info!("Current allow_exit = {}", game_context.is_allow_exit()); + let timestamp = current_timestamp(); + if let Ok(player_id) = game_context.addr_to_id(&player_addr) { + let event = Event::Leave { player_id }; + if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await + { + ports.send(EventFrame::Shutdown).await; + return close_reason; + } + } else { + error!( + "{} Ignore PlayerLeaving, due to can not map the address to id", + env.log_prefix + ); + } + } + EventFrame::RecvBridgeEvent { + event, + dest, + from, + checkpoint, + settle_version, + .. + } => { + // In the case of parent, update the child game's + // checkpoint value. + + let timestamp = current_timestamp(); + + if game_context.game_id() == 0 && dest == 0 && from != 0 && settle_version > 0 { + info!("Update checkpoint for child game: {}", from); + game_context.checkpoint_mut().set_data(from, checkpoint) + } + + if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await { ports.send(EventFrame::Shutdown).await; return close_reason; } } - EventFrame::SendEvent { event } => { - if let Some(close_reason) = - handle(&mut handler, &mut game_context, event, &ports, ctx.mode).await + EventFrame::SendEvent { event, timestamp } => { + if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await { ports.send(EventFrame::Shutdown).await; return close_reason; } } - EventFrame::SendServerEvent { event } => { + EventFrame::SendServerEvent { event, timestamp } => { // Handle the shutdown event from game logic if matches!(event, Event::Shutdown) { ports.send(EventFrame::Shutdown).await; return CloseReason::Complete; - } else if let Some(close_reason) = - handle(&mut handler, &mut game_context, event, &ports, ctx.mode).await + } else if let Some(close_reason) = handle_event( + &mut handler, + &mut game_context, + event, + &ports, + ctx.client_mode, + ctx.game_mode, + timestamp, + &env, + ) + .await { ports.send(EventFrame::Shutdown).await; return close_reason; } } EventFrame::Shutdown => { - warn!("Shutdown event loop"); + warn!("{} Shutdown event loop", env.log_prefix); return CloseReason::Complete; } _ => (), @@ -263,22 +456,17 @@ impl EventLoop { pub fn init( handler: WrappedHandler, game_context: GameContext, - mode: ClientMode, + client_mode: ClientMode, + game_mode: GameMode, ) -> (Self, EventLoopContext) { ( Self {}, EventLoopContext { handler, game_context, - mode, + client_mode, + game_mode, }, ) } } - -fn current_timestamp() -> u64 { - std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64 -} diff --git a/transactor/src/component/submitter.rs b/transactor/src/component/submitter.rs index b8c35ff5..d5cc78c3 100644 --- a/transactor/src/component/submitter.rs +++ b/transactor/src/component/submitter.rs @@ -3,16 +3,22 @@ use std::time::Duration; use async_trait::async_trait; use race_api::error::Error; -use race_core::types::{GameAccount, SettleParams}; +use race_core::storage::StorageT; +use race_core::types::{GameAccount, SaveCheckpointParams, SettleParams, TxState}; use tokio::select; use tokio::sync::mpsc; -use tracing::error; +use tracing::{error, info}; -use crate::component::common::{Component, ConsumerPorts}; +use crate::component::common::Component; use crate::component::event_bus::CloseReason; use crate::frame::EventFrame; use race_core::transport::TransportT; +use super::ComponentEnv; +use super::common::PipelinePorts; + +const MAX_PENDING_TXS: usize = 10; + /// Squash two settles into one. fn squash_settles(mut prev: SettleParams, next: SettleParams) -> SettleParams { let SettleParams { @@ -36,13 +42,13 @@ fn squash_settles(mut prev: SettleParams, next: SettleParams) -> SettleParams { } } -/// Read at most 3 settle events from channel. +/// Read at most `MAX_PENDING_TXS` settle events from channel. async fn read_settle_params(rx: &mut mpsc::Receiver) -> Vec { let mut v = vec![]; let mut cnt = 0; loop { - if cnt == 3 { + if cnt == MAX_PENDING_TXS { break; } @@ -71,6 +77,7 @@ async fn read_settle_params(rx: &mut mpsc::Receiver) -> Vec, + storage: Arc, } pub struct Submitter {} @@ -79,35 +86,48 @@ impl Submitter { pub fn init( game_account: &GameAccount, transport: Arc, + storage: Arc, ) -> (Self, SubmitterContext) { ( Self {}, SubmitterContext { addr: game_account.addr.clone(), transport, + storage, }, ) } } #[async_trait] -impl Component for Submitter { - fn name(&self) -> &str { +impl Component for Submitter { + fn name() -> &'static str { "Submitter" } - async fn run(mut ports: ConsumerPorts, ctx: SubmitterContext) -> CloseReason { + async fn run(mut ports: PipelinePorts, ctx: SubmitterContext, env: ComponentEnv) -> CloseReason { let (queue_tx, mut queue_rx) = mpsc::channel::(32); - + let p = ports.clone_as_producer(); // Start a task to handle settlements // Prevent the blocking from pending transactions let join_handle = tokio::spawn(async move { loop { let ps = read_settle_params(&mut queue_rx).await; if let Some(params) = ps.into_iter().reduce(squash_settles) { + let settle_version = params.settle_version; let res = ctx.transport.settle_game(params).await; match res { - Ok(_) => (), + Ok(signature) => { + let tx_state = TxState::SettleSucceed { + signature: if signature.is_empty() { + None + } else { + Some(signature) + }, + settle_version, + }; + p.send(EventFrame::TxState { tx_state }).await; + } Err(e) => { return CloseReason::Fault(e); } @@ -121,22 +141,48 @@ impl Component for Submitter { while let Some(event) = ports.recv().await { match event { - EventFrame::Settle { + EventFrame::Checkpoint { settles, transfers, checkpoint, settle_version, + previous_settle_version, + .. } => { - let res = queue_tx.send(SettleParams { - addr: ctx.addr.clone(), - settles, - transfers, - checkpoint, + let checkpoint_onchain = checkpoint.derive_onchain_part(); + let checkpoint_offchain = checkpoint.derive_offchain_part(); + + info!("{} Submitter save checkpoint to storage, settle_version = {}", + env.log_prefix, settle_version + ); + let save_checkpoint_result = ctx.storage.save_checkpoint(SaveCheckpointParams { + game_addr: ctx.addr.clone(), settle_version, - next_settle_version: settle_version + 1, + checkpoint: checkpoint_offchain, }).await; + + if let Err(e) = save_checkpoint_result { + error!("{} Submitter failed to save checkpoint offchain: {}", + env.log_prefix, e.to_string()); + break; + } + + let res = queue_tx + .send(SettleParams { + addr: ctx.addr.clone(), + settles, + transfers, + checkpoint: checkpoint_onchain, + settle_version: previous_settle_version, + next_settle_version: settle_version, + }) + .await; if let Err(e) = res { - error!("Submitter failed to send settle to task queue: {}", e.to_string()); + error!( + "{} Submitter failed to send settle to task queue: {}", + env.log_prefix, + e.to_string() + ); } } EventFrame::Shutdown => { @@ -160,42 +206,47 @@ impl Component for Submitter { mod tests { use super::*; - use race_core::types::Settle; + use race_core::{checkpoint::Checkpoint, types::SettleWithAddr}; + use race_local_db::LocalDbStorage; use race_test::prelude::*; #[tokio::test] async fn test_submit_settle() { - let alice = TestClient::player("alice"); - let bob = TestClient::player("bob"); - let charlie = TestClient::player("charlie"); + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); + let mut charlie = TestClient::player("charlie"); let game_account = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .add_player(&bob, 100) - .add_player(&charlie, 100) + .add_player(&mut alice, 100) + .add_player(&mut bob, 100) + .add_player(&mut charlie, 100) .build(); let transport = Arc::new(DummyTransport::default()); - let (submitter, ctx) = Submitter::init(&game_account, transport.clone()); + let storage = Arc::new(LocalDbStorage::try_new_mem().unwrap()); + let (submitter, ctx) = Submitter::init(&game_account, transport.clone(), storage.clone()); let settles = vec![ - Settle::sub("alice", 50), - Settle::add("alice", 20), - Settle::add("alice", 20), - Settle::sub("alice", 40), - Settle::add("bob", 50), - Settle::sub("bob", 20), - Settle::sub("bob", 20), - Settle::sub("bob", 20), - Settle::add("bob", 30), - Settle::eject("charlie"), + SettleWithAddr::sub("alice", 50), + SettleWithAddr::add("alice", 20), + SettleWithAddr::add("alice", 20), + SettleWithAddr::sub("alice", 40), + SettleWithAddr::add("bob", 50), + SettleWithAddr::sub("bob", 20), + SettleWithAddr::sub("bob", 20), + SettleWithAddr::sub("bob", 20), + SettleWithAddr::add("bob", 30), + SettleWithAddr::eject("charlie"), ]; - let event_frame = EventFrame::Settle { + let event_frame = EventFrame::Checkpoint { settles: settles.clone(), transfers: vec![], - checkpoint: vec![], - settle_version: 0, + checkpoint: Checkpoint::default(), + settle_version: 1, + previous_settle_version: 0, + access_version: 1, + state_sha: "".into(), }; - let handle = submitter.start(ctx); + let handle = submitter.start("TEST", ctx); handle.send_unchecked(event_frame).await; handle.send_unchecked(EventFrame::Shutdown).await; diff --git a/transactor/src/component/subscriber.rs b/transactor/src/component/subscriber.rs index 96fefd28..bce734ba 100644 --- a/transactor/src/component/subscriber.rs +++ b/transactor/src/component/subscriber.rs @@ -6,6 +6,7 @@ use async_trait::async_trait; use futures::pin_mut; use futures::StreamExt; use race_core::types::BroadcastFrame; +use race_core::types::BroadcastSync; use race_core::types::VoteType; use race_core::types::{GameAccount, ServerAccount}; use tracing::error; @@ -15,6 +16,7 @@ use tracing::warn; use crate::frame::EventFrame; use super::common::{Component, ProducerPorts}; +use super::ComponentEnv; use super::{event_bus::CloseReason, RemoteConnection}; pub struct SubscriberContext { @@ -52,11 +54,11 @@ impl Subscriber { #[async_trait] impl Component for Subscriber { - fn name(&self) -> &str { + fn name() -> &'static str { "Subscriber" } - async fn run(ports: ProducerPorts, ctx: SubscriberContext) -> CloseReason { + async fn run(ports: ProducerPorts, ctx: SubscriberContext, env: ComponentEnv) -> CloseReason { let SubscriberContext { game_addr, server_addr: _, @@ -76,8 +78,8 @@ impl Component for Subscriber { Err(e) => { if retries == 3 { error!( - "Failed to subscribe events: {}. Vote on the transactor {} has dropped", - e, transactor_addr + "{} Failed to subscribe events: {}. Vote on the transactor {} has dropped", + env.log_prefix, e, transactor_addr ); ports @@ -87,10 +89,13 @@ impl Component for Subscriber { }) .await; - warn!("Shutdown subscriber"); + warn!("{} Shutdown subscriber", env.log_prefix); return CloseReason::Complete; } else { - error!("Failed to subscribe events: {}, will retry", e); + error!( + "{} Failed to subscribe events: {}, will retry", + env.log_prefix, e + ); retries += 1; continue; } @@ -98,29 +103,69 @@ impl Component for Subscriber { } }; - info!("Subscription established"); + info!("{} Subscription established", env.log_prefix); pin_mut!(sub); while let Some(frame) = sub.next().await { match frame { // Forward event to event bus - BroadcastFrame::Event { event, .. } => { - if let Err(e) = ports.try_send(EventFrame::SendServerEvent { event }).await { + BroadcastFrame::Event { event, timestamp, .. } => { + info!("{} Receive event: {}", env.log_prefix, event); + if let Err(e) = ports.try_send(EventFrame::SendServerEvent { event, timestamp }).await { error!("Send server event error: {}", e); break; } } + BroadcastFrame::Sync { sync } => { + let BroadcastSync { + new_players, + new_servers, + access_version, + transactor_addr, + } = sync; + info!( + "{} Receive Sync broadcast, new_players: {:?}, new_servers: {:?}", + env.log_prefix, new_players, new_servers + ); + if let Err(e) = ports + .try_send(EventFrame::Sync { + new_players, + new_servers, + transactor_addr, + access_version, + }) + .await + { + error!("{} Send update node error: {}", env.log_prefix, e); + break; + } + } + BroadcastFrame::Message { .. } => { // Dropped } BroadcastFrame::TxState { .. } => { // Dropped } + BroadcastFrame::EventHistories { histories, .. } => { + info!( + "{} Receive event histories: {}", + env.log_prefix, + histories.len() + ); + for hist in histories { + if let Err(e) = ports.try_send(EventFrame::SendServerEvent { event: hist.event.clone(), timestamp: hist.timestamp }).await + { + error!("Send server event error: {}", e); + break; + } + } + } } } - warn!("Vote for disconnecting"); + warn!("{} Vote for disconnecting", env.log_prefix); ports .send(EventFrame::Vote { votee: transactor_addr, diff --git a/transactor/src/component/synchronizer.rs b/transactor/src/component/synchronizer.rs index 061d2d4a..f2d76b07 100644 --- a/transactor/src/component/synchronizer.rs +++ b/transactor/src/component/synchronizer.rs @@ -1,12 +1,19 @@ -use std::{sync::Arc, time::Duration}; +//! Synchronize the updates from game account. Currently we use pulls +//! instead of websocket. Firstly we query the state with confirming +//! status, if a new state is found, we swtich to query with finalized +//! status. For confirming query we have 5 seconds as interval, for +//! finalized query, we use 2 seconds as interval. + +use std::sync::Arc; use async_trait::async_trait; -use tokio::time::sleep; +use race_api::error::Error; +use tokio_stream::StreamExt; -use crate::{frame::EventFrame, utils::addr_shorthand}; +use crate::frame::EventFrame; use race_core::{ transport::TransportT, - types::{GameAccount, PlayerJoin, QueryMode, ServerJoin, TxState}, + types::{GameAccount, PlayerJoin, QueryMode, ServerJoin}, }; use tracing::{info, warn}; @@ -15,14 +22,17 @@ use crate::component::{ event_bus::CloseReason, }; +use super::ComponentEnv; + pub struct GameSynchronizerContext { transport: Arc, access_version: u64, game_addr: String, + #[allow(unused)] mode: QueryMode, } -const MAX_RETRIES: u8 = 10; +// const MAX_RETRIES: u8 = 10; /// A component that reads the on-chain states and feeds the system. /// To construct a synchronizer, a chain adapter is required. @@ -48,139 +58,212 @@ impl GameSynchronizer { #[allow(unused_assignments)] #[async_trait] impl Component for GameSynchronizer { - fn name(&self) -> &str { - "Game synchronizer" + fn name() -> &'static str { + "Game Synchronizer" } - async fn run(ports: ProducerPorts, ctx: GameSynchronizerContext) -> CloseReason { + async fn run( + ports: ProducerPorts, + ctx: GameSynchronizerContext, + env: ComponentEnv, + ) -> CloseReason { let mut prev_access_version = ctx.access_version; - let mut access_version = ctx.access_version; - let mut mode = ctx.mode; - let mut num_of_retries = 0; - - loop { - let state = ctx - .transport - .get_game_account(&ctx.game_addr, mode) - .await; - - if ports.is_tx_closed() { - return CloseReason::Complete; + + let mut sub = match ctx.transport.subscribe_game_account(&ctx.game_addr).await { + Ok(sub) => sub, + Err(e) => { + warn!( + "{} Synchronizer failed to subscribe game account updates: {}", + env.log_prefix, + e.to_string() + ); + return CloseReason::Fault(Error::GameAccountNotFound); + } + }; + + while let Some(Some(game_account)) = sub.next().await { + let GameAccount { + players, + servers, + access_version, + transactor_addr, + .. + } = game_account; + + // Drop duplicated updates + if access_version <= prev_access_version { + continue; } - match mode { - QueryMode::Confirming => { - if let Ok(Some(state)) = state { - if access_version < state.access_version { - info!( - "{} Synchronizer found confirming state, access_version = {}, settle_version = {}", - addr_shorthand(&ctx.game_addr), - state.access_version, - state.settle_version, - ); - let GameAccount { - access_version: av, - players, - deposits: _, - .. - } = state; - - let confirm_players: Vec = players - .into_iter() - .filter(|p| p.access_version > access_version) - .collect(); - - if !confirm_players.is_empty() { - let tx_state = TxState::PlayerConfirming { - confirm_players, - access_version: state.access_version, - }; - let frame = EventFrame::TxState { tx_state }; - - // When other channels are closed - if ports.try_send(frame).await.is_err() { - return CloseReason::Complete; - } - } - prev_access_version = access_version; - access_version = av; - mode = QueryMode::Finalized; - } else { - sleep(Duration::from_secs(1)).await; - } - } - } + info!( + "{} Synchronizer found new game state, access_version = {}, settle_version = {}", + env.log_prefix, game_account.access_version, game_account.settle_version, + ); - QueryMode::Finalized => { - if let Ok(Some(state)) = state { - let GameAccount { - access_version: av, - players, - deposits: _, - servers, - transactor_addr, - .. - } = state; - - if access_version <= state.access_version { - info!( - "{} Synchronizer found a finalized state, access_version = {}, settle_version = {}", - addr_shorthand(&ctx.game_addr), - state.access_version, - state.settle_version, - ); - let new_players: Vec = players - .into_iter() - .filter(|p| p.access_version > prev_access_version) - .collect(); - - let new_servers: Vec = servers - .into_iter() - .filter(|s| s.access_version > prev_access_version) - .collect(); - - if !new_players.is_empty() || !new_servers.is_empty() { - let frame = EventFrame::Sync { - new_players, - new_servers, - // TODO: Handle transactor addr change - transactor_addr: transactor_addr.unwrap().clone(), - access_version: state.access_version, - }; - - // When other channels are closed - if ports.try_send(frame).await.is_err() { - return CloseReason::Complete; - } - } - num_of_retries = 0; - access_version = av; - mode = QueryMode::Confirming; - } else if num_of_retries < MAX_RETRIES { - num_of_retries += 1; - sleep(Duration::from_secs(1)).await; - } else { - // Signal absence of a new game state - let tx_state = TxState::PlayerConfirmingFailed(access_version); - - let frame = EventFrame::TxState { tx_state }; - - // When other channels are closed - if ports.try_send(frame).await.is_err() { - return CloseReason::Complete; - } - mode = QueryMode::Confirming; - num_of_retries = 0; - access_version = prev_access_version; - } - } else { - warn!("Game account not found, shutdown synchronizer"); - return CloseReason::Complete; - } + let new_players: Vec = players + .into_iter() + .filter(|p| p.access_version > prev_access_version) + .collect(); + + let new_servers: Vec = servers + .into_iter() + .filter(|s| s.access_version > prev_access_version) + .collect(); + + if !new_players.is_empty() || !new_servers.is_empty() { + let frame = EventFrame::Sync { + new_players, + new_servers, + // TODO: Handle transactor addr change + transactor_addr: transactor_addr.unwrap().clone(), + access_version, + }; + + // When other channels are closed + if ports.try_send(frame).await.is_err() { + return CloseReason::Complete; } } + + prev_access_version = access_version; } + + return CloseReason::Complete; } + + // async fn run( + // ports: ProducerPorts, + // ctx: GameSynchronizerContext, + // env: ComponentEnv, + // ) -> CloseReason { + // let mut prev_access_version = ctx.access_version; + // let mut access_version = ctx.access_version; + // let mut mode = ctx.mode; + // let mut num_of_retries = 0; + + // loop { + // let state = ctx.transport.get_game_account(&ctx.game_addr, mode).await; + + // if ports.is_tx_closed() { + // return CloseReason::Complete; + // } + + // match mode { + // QueryMode::Confirming => { + // if let Ok(Some(state)) = state { + // if access_version < state.access_version { + // info!( + // "{} Synchronizer found confirming state, access_version = {}, settle_version = {}", + // env.log_prefix, + // state.access_version, + // state.settle_version, + // ); + // let GameAccount { + // access_version: av, + // players, + // deposits: _, + // .. + // } = state; + + // let confirm_players: Vec = players + // .into_iter() + // .filter(|p| p.access_version > access_version) + // .map(Into::into) + // .collect(); + + // if !confirm_players.is_empty() { + // let tx_state = TxState::PlayerConfirming { + // confirm_players, + // access_version: state.access_version, + // }; + // let frame = EventFrame::TxState { tx_state }; + + // // When other channels are closed + // if ports.try_send(frame).await.is_err() { + // return CloseReason::Complete; + // } + // } + // prev_access_version = access_version; + // access_version = av; + // mode = QueryMode::Finalized; + // } else { + // sleep(Duration::from_secs(5)).await; + // } + // } + // } + + // QueryMode::Finalized => { + // if let Ok(Some(state)) = state { + // let GameAccount { + // access_version: av, + // players, + // deposits: _, + // servers, + // transactor_addr, + // .. + // } = state; + + // if access_version <= state.access_version { + // info!( + // "{} Synchronizer found a finalized state, access_version = {}, settle_version = {}", + // env.log_prefix, + // state.access_version, + // state.settle_version, + // ); + // let new_players: Vec = players + // .into_iter() + // .filter(|p| p.access_version > prev_access_version) + // .collect(); + + // let new_servers: Vec = servers + // .into_iter() + // .filter(|s| s.access_version > prev_access_version) + // .collect(); + + // if !new_players.is_empty() || !new_servers.is_empty() { + // let frame = EventFrame::Sync { + // new_players, + // new_servers, + // // TODO: Handle transactor addr change + // transactor_addr: transactor_addr.unwrap().clone(), + // access_version: state.access_version, + // }; + + // // When other channels are closed + // if ports.try_send(frame).await.is_err() { + // return CloseReason::Complete; + // } + // } + // num_of_retries = 0; + // access_version = av; + // mode = QueryMode::Confirming; + // sleep(Duration::from_secs(5)).await; + // } else if num_of_retries < MAX_RETRIES { + // num_of_retries += 1; + // sleep(Duration::from_secs(2)).await; + // } else { + // // Signal absence of a new game state + // let tx_state = TxState::PlayerConfirmingFailed(access_version); + + // let frame = EventFrame::TxState { tx_state }; + + // // When other channels are closed + // if ports.try_send(frame).await.is_err() { + // return CloseReason::Complete; + // } + // mode = QueryMode::Confirming; + // num_of_retries = 0; + // access_version = prev_access_version; + // sleep(Duration::from_secs(5)).await; + // } + // } else { + // warn!("Game account not found, shutdown synchronizer"); + // return CloseReason::Complete; + // } + // } + // } + // } + // } } #[cfg(test)] @@ -191,19 +274,19 @@ mod tests { #[tokio::test] async fn test_sync_state() { let transport = Arc::new(DummyTransport::default()); - let alice = TestClient::player("alice"); - let bob = TestClient::player("bob"); - let foo = TestClient::transactor("foo"); - let bar = TestClient::validator("bar"); + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); + let mut foo = TestClient::transactor("foo"); + let mut bar = TestClient::validator("bar"); let ga_0 = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .set_transactor(&foo) + .add_player(&mut alice, 100) + .set_transactor(&mut foo) .build(); let ga_1 = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .set_transactor(&foo) - .add_player(&bob, 100) - .add_validator(&bar) + .add_player(&mut alice, 100) + .set_transactor(&mut foo) + .add_player(&mut bob, 100) + .add_validator(&mut bar) .build(); println!("ga_0: {:?}", ga_0); @@ -212,26 +295,30 @@ mod tests { // Use vec to simulate game accounts transport.simulate_states(vec![ga_1.clone(), ga_0.clone(), ga_1.clone()]); let (synchronizer, ctx) = GameSynchronizer::init(transport.clone(), &ga_0); - let mut handle = synchronizer.start(ctx); + let mut handle = synchronizer.start("synchronizer", ctx); let frame = handle.recv_unchecked().await.unwrap(); - let test_confirm_players = vec![PlayerJoin { + let expected_new_players = vec![PlayerJoin { addr: "bob".into(), position: 1, balance: 100, access_version: 3, verify_key: "".into(), }]; - - let test_tx_state = TxState::PlayerConfirming { - confirm_players: test_confirm_players, + let expected_new_servers = vec![ServerJoin { + addr: "bar".into(), + endpoint: "".into(), access_version: 4, - }; + verify_key: "".into(), + }]; + let expected_transactor_addr = "foo".to_string(); + let expected_access_version = 4; - if let EventFrame::TxState { tx_state } = frame { - assert_eq!(tx_state, test_tx_state); - } else { - panic!("Invalid event frame"); + if let EventFrame::Sync { new_players, new_servers, transactor_addr, access_version } = frame { + assert_eq!(new_players, expected_new_players); + assert_eq!(new_servers, expected_new_servers); + assert_eq!(access_version, expected_access_version); + assert_eq!(transactor_addr, expected_transactor_addr); } } } diff --git a/transactor/src/component/voter.rs b/transactor/src/component/voter.rs index b5889d10..75b36078 100644 --- a/transactor/src/component/voter.rs +++ b/transactor/src/component/voter.rs @@ -12,7 +12,7 @@ use race_core::{ }; use tracing::{info, warn}; -use super::common::{Component, PipelinePorts}; +use super::{common::{Component, PipelinePorts}, ComponentEnv}; use crate::frame::EventFrame; use super::event_bus::CloseReason; @@ -44,11 +44,11 @@ impl Voter { #[async_trait] impl Component for Voter { - fn name(&self) -> &str { + fn name() -> &'static str { "Voter" } - async fn run(mut ports: PipelinePorts, ctx: VoterContext) -> CloseReason { + async fn run(mut ports: PipelinePorts, ctx: VoterContext, env: ComponentEnv) -> CloseReason { while let Some(frame) = ports.recv().await { match frame { EventFrame::Vote { votee, vote_type } => { @@ -64,19 +64,19 @@ impl Component for Voter { let r = ctx.transport.vote(params.clone()).await; match r { Ok(_) | Err(Error::DuplicatedVote) => { - info!("Vote sent"); + info!("{} Vote sent", env.log_prefix); ports.send(EventFrame::Shutdown).await; break; } Err(e) => { - warn!("An error occurred in vote: {:?}, will retry.", e); + warn!("{} An error occurred in vote: {:?}, will retry.", env.log_prefix, e); tokio::time::sleep(Duration::from_secs(3)).await; } } } } EventFrame::Shutdown => { - warn!("Shutdown voter"); + warn!("{} Shutdown voter", env.log_prefix); break; } _ => (), diff --git a/transactor/src/component/wrapped_client.rs b/transactor/src/component/wrapped_client.rs index ab7ebc71..a72c5021 100644 --- a/transactor/src/component/wrapped_client.rs +++ b/transactor/src/component/wrapped_client.rs @@ -15,9 +15,10 @@ use race_client::Client; use race_core::connection::ConnectionT; use race_core::encryptor::EncryptorT; use race_core::transport::TransportT; -use race_core::types::{ClientMode, GameAccount, ServerAccount}; -use tracing::warn; +use race_core::types::ClientMode; +use tracing::{error, warn}; +use super::ComponentEnv; use super::event_bus::CloseReason; pub struct WrappedClient {} @@ -33,24 +34,13 @@ pub struct ClientContext { impl WrappedClient { pub fn init( - server_account: &ServerAccount, - init_account: &GameAccount, + addr: String, + game_addr: String, + mode: ClientMode, transport: Arc, encryptor: Arc, connection: Arc, ) -> (Self, ClientContext) { - // Detect our client mode by check if our address is the transactor address - let server_addr = server_account.addr.clone(); - let mode = if server_addr.eq(init_account - .transactor_addr - .as_ref() - .expect("Game is not served")) - { - ClientMode::Transactor - } else { - ClientMode::Validator - }; - ( Self {}, ClientContext { @@ -58,8 +48,8 @@ impl WrappedClient { encryptor, connection, mode, - addr: server_addr, - game_addr: init_account.addr.to_owned(), + addr, + game_addr, }, ) } @@ -67,11 +57,11 @@ impl WrappedClient { #[async_trait] impl Component for WrappedClient { - fn name(&self) -> &str { + fn name() -> &'static str { "Client" } - async fn run(mut ports: ConsumerPorts, ctx: ClientContext) -> CloseReason { + async fn run(mut ports: ConsumerPorts, ctx: ClientContext, env: ComponentEnv) -> CloseReason { let ClientContext { addr, game_addr, @@ -84,7 +74,7 @@ impl Component for WrappedClient { let mut client = Client::new(addr, game_addr, mode, transport, encryptor, connection); if let Err(e) = client.attach_game().await { - warn!("Failed to attach to game due to error: {:?}", e); + warn!("{} Failed to attach to game due to error: {:?}", env.log_prefix, e); } let mut res = Ok(()); @@ -99,16 +89,20 @@ impl Component for WrappedClient { break 'outer; } } - if context.is_checkpoint() { - client.flush_secret_states(); - } + // if context.is_checkpoint() { + // client.flush_secret_states(); + // } } Err(e) => { + error!("{} Client error: {:?}", env.log_prefix, e); res = Err(e); break 'outer; } } } + EventFrame::Checkpoint { .. } => { + client.flush_secret_states(); + } EventFrame::Shutdown => break, _ => (), } @@ -125,6 +119,7 @@ impl Component for WrappedClient { mod tests { use race_api::prelude::*; + use race_core::types::ServerAccount; use race_encryptor::Encryptor; use race_test::prelude::*; @@ -137,38 +132,40 @@ mod tests { GameContext, PortsHandle, Arc, + TestClient, ) { - let alice = TestClient::player("alice"); - let bob = TestClient::player("bob"); - let transactor = TestClient::transactor("transactor"); + let mut alice = TestClient::player("alice"); + let mut bob = TestClient::player("bob"); + let mut transactor = TestClient::transactor("transactor"); let game_account = TestGameAccountBuilder::default() - .add_player(&alice, 100) - .add_player(&bob, 100) - .set_transactor(&transactor) + .add_player(&mut alice, 100) + .add_player(&mut bob, 100) + .set_transactor(&mut transactor) .build(); let encryptor = Arc::new(Encryptor::default()); let transactor_account = ServerAccount { - addr: transactor.get_addr(), + addr: transactor.addr(), endpoint: "".into(), }; let connection = Arc::new(DummyConnection::default()); let transport = Arc::new(DummyTransport::default()); let (client, client_ctx) = WrappedClient::init( - &transactor_account, - &game_account, + transactor_account.addr.clone(), + game_account.addr.clone(), + ClientMode::Transactor, transport, encryptor, connection.clone(), ); - let handle = client.start(client_ctx); - let mut context = GameContext::try_new(&game_account).unwrap(); + let handle = client.start(&game_account.addr, client_ctx); + let mut context = GameContext::try_new(&game_account, None).unwrap(); context.set_node_ready(game_account.access_version); - (client, context, handle, connection) + (client, context, handle, connection, transactor) } #[tokio::test(flavor = "multi_thread")] async fn test_lock() { - let (mut _client, mut ctx, handle, connection) = setup(); + let (mut _client, mut ctx, handle, connection, tx) = setup(); // Mask the random_state let random = RandomSpec::shuffled_list(vec!["a".into(), "b".into(), "c".into()]); @@ -178,7 +175,9 @@ mod tests { .mask("transactor".to_string(), vec![vec![0], vec![0], vec![0]]) .unwrap(); - let event_frame = EventFrame::ContextUpdated { context: Box::new(ctx) }; + let event_frame = EventFrame::ContextUpdated { + context: Box::new(ctx), + }; handle.send_unchecked(event_frame).await; println!("before read event"); @@ -190,7 +189,7 @@ mod tests { ciphertexts_and_digests, } => { assert_eq!(rid, random_id); - assert_eq!(sender, "transactor".to_string()); + assert_eq!(sender, tx.id()); assert_eq!(3, ciphertexts_and_digests.len()); } _ => panic!("Invalid event type"), @@ -199,14 +198,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_mask() { - let (mut _client, mut ctx, handle, connection) = setup(); + let (mut _client, mut ctx, handle, connection, tx) = setup(); let random = RandomSpec::shuffled_list(vec!["a".into(), "b".into(), "c".into()]); println!("context: {:?}", ctx); let rid = ctx.init_random_state(random).unwrap(); println!("random inited"); - let event_frame = EventFrame::ContextUpdated { context: Box::new(ctx) }; + let event_frame = EventFrame::ContextUpdated { + context: Box::new(ctx), + }; handle.send_unchecked(event_frame).await; println!("before read event"); @@ -219,7 +220,7 @@ mod tests { ciphertexts, } => { assert_eq!(rid, random_id); - assert_eq!(sender, "transactor".to_string()); + assert_eq!(sender, tx.id()); assert_eq!(3, ciphertexts.len()); } _ => panic!("Invalid event type"), diff --git a/transactor/src/component/wrapped_handler.rs b/transactor/src/component/wrapped_handler.rs index 6c95f7df..6c25cf08 100644 --- a/transactor/src/component/wrapped_handler.rs +++ b/transactor/src/component/wrapped_handler.rs @@ -2,17 +2,18 @@ use std::mem::swap; use std::path::PathBuf; use std::sync::Arc; -use borsh::{BorshDeserialize, BorshSerialize}; +use borsh::BorshDeserialize; use race_api::effect::Effect; -use race_api::engine::InitAccount; use race_api::error::{Error, Result}; use race_api::event::Event; -use race_core::context::GameContext; +use race_api::init_account::InitAccount; +use race_core::context::{EventEffects, GameContext}; use race_core::encryptor::EncryptorT; -use race_core::engine::{general_handle_event, general_init_state, post_handle_event}; -use race_core::types::{GameBundle, Settle, Transfer}; +use race_core::engine::general_handle_event; +use race_core::types::GameBundle; use race_encryptor::Encryptor; use tracing::info; +use tracing::log::error; use wasmer::{imports, Instance, Module, Store, TypedFunction}; fn log_execution_context(effect_bs: &Vec, event_bs: &Vec) { @@ -30,13 +31,6 @@ pub struct WrappedHandler { encryptor: Arc, } -#[derive(Debug)] -pub struct Effects { - pub settles: Vec, - pub transfers: Vec, - pub checkpoint: Vec, -} - impl WrappedHandler { /// Load WASM bundle by game address pub async fn load_by_bundle( @@ -75,7 +69,7 @@ impl WrappedHandler { &mut self, context: &mut GameContext, init_account: &InitAccount, - ) -> Result<()> { + ) -> Result { let memory = self .instance .exports @@ -91,12 +85,10 @@ impl WrappedHandler { .get_typed_function(&self.store, "init_state") .map_err(|e| Error::WasmInitializationError(e.to_string()))?; let mem_view = memory.view(&self.store); - let effect = context.derive_effect(); - let effect_bs = effect - .try_to_vec() - .map_err(|e| Error::WasmInitializationError(e.to_string()))?; - let init_account_bs = init_account - .try_to_vec() + let effect = context.derive_effect(true); + let effect_bs = + borsh::to_vec(&effect).map_err(|e| Error::WasmInitializationError(e.to_string()))?; + let init_account_bs = borsh::to_vec(&init_account) .map_err(|e| Error::WasmInitializationError(e.to_string()))?; let mut offset = 1u32; mem_view @@ -116,18 +108,23 @@ impl WrappedHandler { ) .map_err(|e| Error::WasmInitializationError(e.to_string()))?; - if len == 0 { - return Err(Error::WasmInitializationError( - "Seriliazing effect failed".into(), - )); - } else if len == 1 { - return Err(Error::WasmInitializationError( - "Deserializing effect failed".into(), - )); - } else if len == 2 { - return Err(Error::WasmInitializationError( - "Deserializing event failed".into(), - )); + match len { + 0 => { + return Err(Error::WasmInitializationError( + "Serializing effect failed".into(), + )) + } + 1 => { + return Err(Error::WasmInitializationError( + "Deserializing effect failed".into(), + )) + } + 2 => { + return Err(Error::WasmInitializationError( + "Deserializing event failed".into(), + )) + } + _ => (), } let mut buf = vec![0; len as _]; @@ -144,7 +141,11 @@ impl WrappedHandler { } } - fn custom_handle_event(&mut self, context: &mut GameContext, event: &Event) -> Result<()> { + fn custom_handle_event( + &mut self, + context: &mut GameContext, + event: &Event, + ) -> Result { let memory = self .instance .exports @@ -156,13 +157,11 @@ impl WrappedHandler { .get_typed_function(&self.store, "handle_event") .map_err(|e| Error::WasmExecutionError(e.to_string()))?; let mem_view = memory.view(&self.store); - let effect = context.derive_effect(); - let effect_bs = effect - .try_to_vec() - .map_err(|e| Error::WasmExecutionError(e.to_string()))?; - let event_bs = event - .try_to_vec() - .map_err(|e| Error::WasmExecutionError(e.to_string()))?; + let effect = context.derive_effect(false); + let effect_bs = + borsh::to_vec(&effect).map_err(|e| Error::WasmExecutionError(e.to_string()))?; + let event_bs = + borsh::to_vec(&event).map_err(|e| Error::WasmExecutionError(e.to_string()))?; let mut offset = 1u32; mem_view .write(offset as _, &effect_bs) @@ -180,8 +179,26 @@ impl WrappedHandler { Error::WasmExecutionError(e.to_string()) })?; - if len == 0 { - return Err(Error::WasmExecutionError("Internal error".into())); + match len { + 0 => { + error!("Effect: {:?}", effect_bs); + return Err(Error::WasmExecutionError( + "Serializing effect failed".into(), + )); + } + 1 => { + error!("Effect: {:?}", effect_bs); + return Err(Error::WasmExecutionError( + "Deserializing effect failed".into(), + )); + } + 2 => { + error!("Effect: {:?}", effect_bs); + return Err(Error::WasmExecutionError( + "Deserializing event failed".into(), + )); + } + _ => (), } let mut buf = vec![0; len as _]; @@ -192,6 +209,7 @@ impl WrappedHandler { let mut effect = Effect::try_from_slice(&buf).map_err(|e| Error::WasmExecutionError(e.to_string()))?; if let Some(e) = effect.__take_error() { + error!("Effect: {:?}", effect_bs); Err(e.into()) } else { context.apply_effect(effect) @@ -202,36 +220,35 @@ impl WrappedHandler { &mut self, context: &mut GameContext, event: &Event, - ) -> Result> { + ) -> Result { let mut new_context = context.clone(); general_handle_event(&mut new_context, event, self.encryptor.as_ref())?; - self.custom_handle_event(&mut new_context, event)?; - post_handle_event(context, &mut new_context)?; - let settles_and_transfers = new_context.take_settles_and_transfers()?; + let event_effects = self.custom_handle_event(&mut new_context, event)?; swap(context, &mut new_context); - if let Some((settles, transfers, checkpoint)) = settles_and_transfers { - Ok(Some(Effects { settles, transfers, checkpoint })) - } else { - Ok(None) - } + Ok(event_effects) } + // Initialize game state pub fn init_state( &mut self, context: &mut GameContext, init_account: &InitAccount, - ) -> Result<()> { + ) -> Result { let mut new_context = context.clone(); - general_init_state(&mut new_context, init_account)?; - self.custom_init_state(&mut new_context, init_account)?; + new_context.set_timestamp(0); + let event_effects = self.custom_init_state(&mut new_context, init_account)?; swap(context, &mut new_context); - Ok(()) + Ok(event_effects) } } #[cfg(test)] mod tests { - use race_api::{prelude::{CustomEvent, HandleError}, types::GameStatus}; + use borsh::BorshSerialize; + use race_api::{ + prelude::{CustomEvent, HandleError}, + types::GameStatus, + }; use race_test::prelude::*; use super::*; @@ -252,13 +269,14 @@ mod tests { } } - fn make_game_account() -> GameAccount { + fn setup_game() -> (GameAccount, TestClient) { let data = MinimalAccountData { init_n: 42 }; - let transactor = TestClient::transactor("transactor"); - TestGameAccountBuilder::default() - .set_transactor(&transactor) + let mut transactor = TestClient::transactor("transactor"); + let acc = TestGameAccountBuilder::default() + .set_transactor(&mut transactor) .with_data(data) - .build() + .build(); + (acc, transactor) } fn make_wrapped_handler() -> WrappedHandler { @@ -270,9 +288,9 @@ mod tests { #[test] fn test_init_state() { let mut hdlr = make_wrapped_handler(); - let game_account = make_game_account(); - let init_account = game_account.derive_init_account(); - let mut ctx = GameContext::try_new(&game_account).unwrap(); + let (game_account, _tx) = setup_game(); + let mut ctx = GameContext::try_new(&game_account, None).unwrap(); + let init_account = ctx.init_account().unwrap(); hdlr.init_state(&mut ctx, &init_account).unwrap(); assert_eq!( &vec![42u8, 0, 0, 0, 0, 0, 0, 0], @@ -283,12 +301,10 @@ mod tests { #[test] fn test_handle_event() { let mut hdlr = make_wrapped_handler(); - let game_account = make_game_account(); - let mut ctx = GameContext::try_new(&game_account).unwrap(); - let event = Event::GameStart { - access_version: game_account.access_version, - }; - let init_account = game_account.derive_init_account(); + let (game_account, _tx) = setup_game(); + let mut ctx = GameContext::try_new(&game_account, None).unwrap(); + let event = Event::GameStart; + let init_account = ctx.init_account().unwrap(); hdlr.init_state(&mut ctx, &init_account).unwrap(); println!("ctx: {:?}", ctx); hdlr.handle_event(&mut ctx, &event).unwrap(); @@ -302,10 +318,10 @@ mod tests { #[test] fn test_handle_custom_event() { let mut hdlr = make_wrapped_handler(); - let game_account = make_game_account(); - let mut ctx = GameContext::try_new(&game_account).unwrap(); - let event = Event::custom("Alice", &MinimalEvent::Increment(1)); - let init_account = game_account.derive_init_account(); + let (game_account, _tx) = setup_game(); + let mut ctx = GameContext::try_new(&game_account, None).unwrap(); + let event = Event::custom(0, &MinimalEvent::Increment(1)); + let init_account = ctx.init_account().unwrap(); hdlr.init_state(&mut ctx, &init_account).unwrap(); println!("ctx: {:?}", ctx); hdlr.handle_event(&mut ctx, &event).unwrap(); diff --git a/transactor/src/component/wrapped_storage.rs b/transactor/src/component/wrapped_storage.rs new file mode 100644 index 00000000..3f515cef --- /dev/null +++ b/transactor/src/component/wrapped_storage.rs @@ -0,0 +1,32 @@ +use race_core::{checkpoint::CheckpointOffChain, storage::StorageT, types::{GetCheckpointParams, SaveCheckpointParams}}; +use race_env::Config; +use jsonrpsee::core::async_trait; +use race_api::error::Result; +use race_local_db::LocalDbStorage; + +pub struct WrappedStorage { + pub(crate) inner: Box, +} + +impl WrappedStorage { + pub async fn try_new(config: &Config) -> Result { + let storage = if let Some(ref storage_config) = config.storage { + LocalDbStorage::try_new(&storage_config.db_file_name)? + } else { + LocalDbStorage::try_new_mem()? + }; + + Ok(Self { inner: Box::new(storage) }) + } +} + +#[async_trait] +impl StorageT for WrappedStorage { + async fn save_checkpoint(&self, params: SaveCheckpointParams) -> Result<()> { + self.inner.save_checkpoint(params).await + } + + async fn get_checkpoint(&self, params: GetCheckpointParams) -> Result> { + self.inner.get_checkpoint(params).await + } +} diff --git a/transactor/src/component/wrapped_transport.rs b/transactor/src/component/wrapped_transport.rs index a9c6b6a2..7595182c 100644 --- a/transactor/src/component/wrapped_transport.rs +++ b/transactor/src/component/wrapped_transport.rs @@ -1,5 +1,6 @@ //! Wrapped transport, which support retry +use futures::Stream; use jsonrpsee::core::async_trait; use race_api::error::Result; use race_core::types::{ @@ -16,13 +17,15 @@ use race_core::{ }; use race_env::Config; use race_transport::TransportBuilder; +use std::pin::Pin; use std::time::Duration; use tracing::error; -const RETRY_INTERVAL: u64 = 10; +const DEFAULT_RETRY_INTERVAL: u64 = 10_000; pub struct WrappedTransport { pub(crate) inner: Box, + retry_interval: u64, } impl WrappedTransport { @@ -37,12 +40,16 @@ impl WrappedTransport { .try_with_config(config)? .build() .await?; - Ok(Self { inner: transport }) + Ok(Self { inner: transport, retry_interval: DEFAULT_RETRY_INTERVAL }) } } #[async_trait] impl TransportT for WrappedTransport { + async fn subscribe_game_account<'a>(&'a self, addr: &'a str) -> Result> + Send + 'a>>> { + self.inner.subscribe_game_account(addr).await + } + async fn create_game_account(&self, params: CreateGameAccountParams) -> Result { self.inner.create_game_account(params).await } @@ -93,46 +100,51 @@ impl TransportT for WrappedTransport { /// `settle_version` is used to identify the settle state, /// Until the `settle_version` is bumped, we keep retrying. - async fn settle_game(&self, params: SettleParams) -> Result<()> { + async fn settle_game(&self, params: SettleParams) -> Result { let mut curr_settle_version: Option = None; loop { let game_account = self .inner .get_game_account(¶ms.addr, QueryMode::Finalized) .await; + println!("Game Account: {:?}", game_account); // -------------- if let Ok(Some(game_account)) = game_account { // We got an old state, which has a smaller `settle_version` if game_account.settle_version < params.settle_version { error!( "Got invalid settle_version: {} != {}, will retry in {} secs", - game_account.settle_version, params.settle_version, RETRY_INTERVAL + game_account.settle_version, params.settle_version, self.retry_interval ); - tokio::time::sleep(Duration::from_secs(RETRY_INTERVAL)).await; + tokio::time::sleep(Duration::from_millis(self.retry_interval)).await; continue; } - // The `settle_version` had been bumped, indicates the transaction was succeed - // NOTE: The transaction can success with error result due to unstable network + // The `settle_version` had been bumped, which + // indicates the transaction was succeed + + //NOTE: The transaction may success with error result + // due to unstable network if curr_settle_version.is_some_and(|v| v < game_account.settle_version) { - return Ok(()); + return Ok("".into()); } curr_settle_version = Some(game_account.settle_version); - if let Err(e) = self.inner.settle_game(params.clone()).await { - error!( - "Error in settlement: {:?}, will retry in {} secs", - e, RETRY_INTERVAL - ); - tokio::time::sleep(Duration::from_secs(RETRY_INTERVAL)).await; - continue; - } else { - return Ok(()); + match self.inner.settle_game(params.clone()).await { + Ok(sig) => return Ok(sig), + Err(e) => { + error!( + "Error in settlement: {:?}, will retry in {} secs", + e, self.retry_interval + ); + tokio::time::sleep(Duration::from_millis(self.retry_interval)).await; + continue; + } } } else { error!( "Error in settlement due to unable to get game account {}, will retry in {} secs", params.addr, - RETRY_INTERVAL + self.retry_interval ); - tokio::time::sleep(Duration::from_secs(RETRY_INTERVAL)).await; + tokio::time::sleep(Duration::from_millis(self.retry_interval)).await; continue; } } @@ -177,6 +189,7 @@ impl TransportT for WrappedTransport { #[cfg(test)] mod tests { + use race_core::checkpoint::CheckpointOnChain; use race_test::prelude::{test_game_addr, DummyTransport, TestGameAccountBuilder}; use super::*; @@ -188,19 +201,19 @@ mod tests { let mut ga1 = TestGameAccountBuilder::new().build(); ga1.settle_version = 1; t.simulate_states(vec![ga0, ga1]); - let wt = WrappedTransport { inner: Box::new(t) }; + let wt = WrappedTransport { inner: Box::new(t), retry_interval: 1 }; let r = wt .settle_game(SettleParams { addr: test_game_addr(), settles: vec![], transfers: vec![], - checkpoint: vec![], + checkpoint: CheckpointOnChain::default(), settle_version: 1, next_settle_version: 2, }) .await; - assert_eq!(r, Ok(())); + assert_eq!(r, Ok("".to_string())); Ok(()) } @@ -212,19 +225,19 @@ mod tests { let mut ga1 = TestGameAccountBuilder::new().build(); ga1.settle_version = 1; t.simulate_states(vec![ga0, ga1]); - let wt = WrappedTransport { inner: Box::new(t) }; + let wt = WrappedTransport { inner: Box::new(t), retry_interval: 1 }; let r = wt .settle_game(SettleParams { addr: test_game_addr(), - settles: vec![], transfers: vec![], - checkpoint: vec![], - settle_version: 1, + settles: vec![], + checkpoint: CheckpointOnChain::default(), + settle_version: 0, next_settle_version: 2, }) .await; - assert_eq!(r, Ok(())); + assert_eq!(r, Ok("".to_string())); Ok(()) } } diff --git a/transactor/src/context.rs b/transactor/src/context.rs index 96c0c3fa..944df672 100644 --- a/transactor/src/context.rs +++ b/transactor/src/context.rs @@ -1,10 +1,10 @@ use crate::blacklist::Blacklist; -use crate::component::WrappedTransport; +use crate::component::{WrappedStorage, WrappedTransport}; use crate::frame::SignalFrame; use crate::game_manager::GameManager; -use race_core::encryptor::{EncryptorT, NodePublicKeyRaw}; use race_api::error::{Error, Result}; use race_api::event::{Event, Message}; +use race_core::encryptor::{EncryptorT, NodePublicKeyRaw}; use race_core::transport::TransportT; use race_core::types::{BroadcastFrame, ServerAccount, Signature}; use race_encryptor::Encryptor; @@ -32,6 +32,8 @@ impl ApplicationContext { let transport = Arc::new(WrappedTransport::try_new(&config).await?); + let storage = Arc::new(WrappedStorage::try_new(&config).await?); + let encryptor = Arc::new(Encryptor::default()); let transactor_config = config.transactor.ok_or(Error::TransactorConfigMissing)?; @@ -45,32 +47,70 @@ impl ApplicationContext { .await? .ok_or(Error::ServerAccountMissing)?; + let debug_mode = transactor_config.debug_mode.unwrap_or(false); + let game_manager = Arc::new(GameManager::default()); - let game_manager_1 = game_manager.clone(); let (signal_tx, mut signal_rx) = mpsc::channel(3); - let transport_1 = transport.clone(); - let encryptor_1 = encryptor.clone(); - let account_1 = account.clone(); - let blacklist = Arc::new(Mutex::new(Blacklist::new(transactor_config.disable_blacklist.ne(&(Some(true)))))); - let blacklist_1 = blacklist.clone(); + let blacklist = Arc::new(Mutex::new(Blacklist::new( + transactor_config.disable_blacklist.ne(&(Some(true))), + ))); + + let game_manager_0 = game_manager.clone(); + let transport_0 = transport.clone(); + let storage_0 = storage.clone(); + let encryptor_0 = encryptor.clone(); + let account_0 = account.clone(); + let blacklist_0 = blacklist.clone(); + let signal_tx_0 = signal_tx.clone(); tokio::spawn(async move { while let Some(signal) = signal_rx.recv().await { - match signal { - SignalFrame::StartGame { game_addr } => { - game_manager_1 - .load_game( - game_addr, - transport_1.clone(), - encryptor_1.clone(), - &account_1, - blacklist_1.clone(), - ) - .await; + let game_manager_1 = game_manager_0.clone(); + let transport_1 = transport_0.clone(); + let storage_1 = storage_0.clone(); + let encryptor_1 = encryptor_0.clone(); + let account_1 = account_0.clone(); + let blacklist_1 = blacklist_0.clone(); + let signal_tx_1 = signal_tx_0.clone(); + tokio::spawn(async move { + match signal { + SignalFrame::StartGame { game_addr } => { + game_manager_1 + .load_game( + game_addr, + transport_1.clone(), + storage_1.clone(), + encryptor_1.clone(), + &account_1, + blacklist_1.clone(), + signal_tx_1.clone(), + debug_mode, + ) + .await; + } + SignalFrame::LaunchSubGame { spec } => { + let bridge_parent = game_manager_1 + .get_event_parent(&spec.game_addr) + .await + .expect( + format!("Bridge parent not found: {}", spec.game_addr).as_str(), + ); + + game_manager_1 + .launch_sub_game( + spec, + bridge_parent, + &account_1, + transport_1.clone(), + encryptor_1.clone(), + debug_mode, + ) + .await; + } } - } + }); } }); @@ -87,7 +127,6 @@ impl ApplicationContext { } pub async fn register_key(&self, player_addr: String, key: NodePublicKeyRaw) -> Result<()> { - // info!("Client {:?} register public key, {:?}", player_addr, key); self.encryptor.add_public_key(player_addr, &key)?; Ok(()) } diff --git a/transactor/src/frame.rs b/transactor/src/frame.rs index bf7c9b9c..1f8d573d 100644 --- a/transactor/src/frame.rs +++ b/transactor/src/frame.rs @@ -1,21 +1,25 @@ use borsh::{BorshDeserialize, BorshSerialize}; use race_api::{ - engine::InitAccount, - event::{Event, Message}, + init_account::InitAccount, + event::{Event, Message} }; use race_core::{ context::GameContext, - types::{PlayerJoin, ServerJoin, Settle, Transfer, TxState, VoteType}, + types::{PlayerJoin, ServerJoin, SettleWithAddr, SubGameSpec, Transfer, TxState, VoteType}, checkpoint::Checkpoint, }; #[derive(Debug, Clone)] pub enum SignalFrame { StartGame { game_addr: String }, + LaunchSubGame { spec: SubGameSpec }, } #[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] pub enum EventFrame { Empty, + GameStart { + access_version: u64, + }, Sync { new_players: Vec, new_servers: Vec, @@ -33,35 +37,36 @@ pub enum EventFrame { player_addr: String, }, InitState { + access_version: u64, + settle_version: u64, init_account: InitAccount, }, SendEvent { event: Event, + timestamp: u64, }, SendMessage { message: Message, }, SendServerEvent { event: Event, + timestamp: u64, }, Checkpoint { + settles: Vec, + transfers: Vec, + checkpoint: Checkpoint, access_version: u64, settle_version: u64, + previous_settle_version: u64, + state_sha: String, }, Broadcast { event: Event, access_version: u64, settle_version: u64, timestamp: u64, - }, - Settle { - settles: Vec, - transfers: Vec, - checkpoint: Vec, - settle_version: u64, - }, - SettleFinalized { - settle_version: u64, + state_sha: String, }, ContextUpdated { context: Box, @@ -71,16 +76,44 @@ pub enum EventFrame { vote_type: VoteType, }, Shutdown, + /// Represent a event send in current event bus. `from` is the + /// source of event, `dest` is the target of the event. value 0 + /// represent the master game. When there's an available + /// checkpoint in the context, it will be sent along with + /// `checkpoint`. + SendBridgeEvent { + from: usize, + dest: usize, + event: Event, + access_version: u64, + settle_version: u64, + checkpoint: Vec, + }, + /// Similar to `SendBridgeEvent`, but for receiver's event bus. + RecvBridgeEvent { + from: usize, + dest: usize, + event: Event, + access_version: u64, + settle_version: u64, + checkpoint: Vec, + }, + LaunchSubGame { + spec: Box, + }, } impl std::fmt::Display for EventFrame { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { EventFrame::Empty => write!(f, "Empty"), - EventFrame::InitState { init_account, .. } => write!( + EventFrame::GameStart { access_version } => { + write!(f, "GameStart, access_version = {}", access_version) + } + EventFrame::InitState { access_version, settle_version, .. } => write!( f, "InitState, access_version = {}, settle_version = {}", - init_account.access_version, init_account.settle_version + access_version, settle_version ), EventFrame::Sync { new_players, @@ -103,18 +136,25 @@ impl std::fmt::Display for EventFrame { ), EventFrame::PlayerDeposited { .. } => write!(f, "PlayerDeposited"), EventFrame::PlayerLeaving { .. } => write!(f, "PlayerLeaving"), - EventFrame::SendEvent { event } => write!(f, "SendEvent: {}", event), - EventFrame::SendServerEvent { event } => write!(f, "SendServerEvent: {}", event), + EventFrame::SendEvent { event, .. } => write!(f, "SendEvent: {}", event), + EventFrame::SendServerEvent { event, .. } => write!(f, "SendServerEvent: {}", event), EventFrame::Checkpoint { .. } => write!(f, "Checkpoint"), EventFrame::Broadcast { event, .. } => write!(f, "Broadcast: {}", event), - EventFrame::Settle { .. } => write!(f, "Settle"), - EventFrame::SettleFinalized { .. } => write!(f, "SettleFinalized"), EventFrame::SendMessage { message } => write!(f, "SendMessage: {}", message.sender), EventFrame::ContextUpdated { context: _ } => write!(f, "ContextUpdated"), EventFrame::Shutdown => write!(f, "Shutdown"), EventFrame::Vote { votee, vote_type } => { write!(f, "Vote: to {} for {:?}", votee, vote_type) } + EventFrame::SendBridgeEvent { dest, event, settle_version, .. } => { + write!(f, "SendBridgeEvent: dest {}, settle_version: {}, event: {}", dest, settle_version, event) + } + EventFrame::RecvBridgeEvent { dest, event, settle_version, .. } => { + write!(f, "RecvBridgeEvent: dest {}, settle_version: {}, event: {}", dest, settle_version, event) + } + EventFrame::LaunchSubGame { spec } => { + write!(f, "LaunchSubGame: {:?}", spec) + } } } } diff --git a/transactor/src/game_manager.rs b/transactor/src/game_manager.rs index 1b9e913d..40b2cf03 100644 --- a/transactor/src/game_manager.rs +++ b/transactor/src/game_manager.rs @@ -1,17 +1,20 @@ use race_api::error::{Error, Result}; use race_api::event::{Event, Message}; -use race_core::types::{BroadcastFrame, ServerAccount}; +use race_core::checkpoint::CheckpointOffChain; +use race_core::types::{BroadcastFrame, ServerAccount, SubGameSpec}; use race_encryptor::Encryptor; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::{broadcast, Mutex}; +use tokio::sync::{broadcast, Mutex, mpsc}; +use tokio::task::JoinHandle; use tracing::{error, info, warn}; use crate::blacklist::Blacklist; -use crate::component::{CloseReason, WrappedTransport}; -use crate::frame::EventFrame; +use crate::component::{CloseReason, EventBridgeParent, WrappedStorage, WrappedTransport}; +use crate::frame::{EventFrame, SignalFrame}; use crate::handle::Handle; +use crate::utils::current_timestamp; pub struct GameManager { games: Arc>>, @@ -25,43 +28,88 @@ impl Default for GameManager { } } +fn wait_and_unload( + game_addr: String, + join_handle: JoinHandle, + games: Arc>>, + blacklist: Option>>, +) { + // Wait and unload + tokio::spawn(async move { + match join_handle.await { + Ok(CloseReason::Complete) => { + let mut games = games.lock().await; + games.remove(&game_addr); + info!("Clean game handle: {}", game_addr); + } + Ok(CloseReason::Fault(_)) => { + let mut games = games.lock().await; + games.remove(&game_addr); + if let Some(blacklist) = blacklist { + blacklist.lock().await.add_addr(&game_addr); + info!("Game stopped with error, clean game handle: {}", game_addr); + } + } + Err(e) => { + error!("Unexpected error when waiting game to stop: {}", e); + } + } + }); +} + impl GameManager { + /// Load a sub game + pub async fn launch_sub_game( + &self, + spec: SubGameSpec, + bridge_parent: EventBridgeParent, + server_account: &ServerAccount, + transport: Arc, + encryptor: Arc, + debug_mode: bool, + ) { + let game_addr = spec.game_addr.clone(); + let game_id = spec.game_id; + match Handle::try_new_sub_game_handle(spec, bridge_parent, server_account, encryptor, transport, debug_mode).await { + Ok(mut handle) => { + let mut games = self.games.lock().await; + let addr = format!("{}:{}", game_addr, game_id); + info!("Launch sub game {}", addr); + let join_handle = handle.wait(); + games.insert(addr.clone(), handle); + wait_and_unload(addr, join_handle, self.games.clone(), None); + } + Err(e) => { + warn!( + "Error loading sub game with id {}: {}", + game_id, + e.to_string() + ); + } + } + } + /// Load game by its address. This operation is idempotent. pub async fn load_game( &self, game_addr: String, transport: Arc, + storage: Arc, encryptor: Arc, server_account: &ServerAccount, blacklist: Arc>, + signal_tx: mpsc::Sender, + debug_mode: bool, ) { let mut games = self.games.lock().await; if let Entry::Vacant(e) = games.entry(game_addr.clone()) { - match Handle::try_new(transport, encryptor, server_account, e.key()).await { + match Handle::try_new(transport, storage, encryptor, server_account, e.key(), signal_tx, debug_mode).await { Ok(mut handle) => { info!("Game handle created: {}", e.key()); let join_handle = handle.wait(); e.insert(handle); - let games = self.games.clone(); - // Wait and unload - tokio::spawn(async move { - match join_handle.await { - Ok(CloseReason::Complete) => { - let mut games = games.lock().await; - games.remove(&game_addr); - info!("Clean game handle: {}", game_addr); - } - Ok(CloseReason::Fault(_)) => { - let mut games = games.lock().await; - games.remove(&game_addr); - blacklist.lock().await.add_addr(&game_addr); - info!("Game stopped with error, clean game handle: {}", game_addr); - } - Err(e) => { - error!("Unexpected error when waiting game to stop: {}", e); - } - } - }); + // TODO: A better handle for errors in initialization + wait_and_unload(game_addr, join_handle, self.games.clone(), Some(blacklist)); } Err(err) => { warn!("Error loading game: {}", err.to_string()); @@ -74,13 +122,14 @@ impl GameManager { pub async fn is_game_loaded(&self, game_addr: &str) -> bool { let games = self.games.lock().await; - games.get(game_addr).is_some() + games.contains_key(game_addr) } pub async fn send_event(&self, game_addr: &str, event: Event) -> Result<()> { let games = self.games.lock().await; if let Some(handle) = games.get(game_addr) { - let event_frame = EventFrame::SendEvent { event }; + let timestamp = current_timestamp(); + let event_frame = EventFrame::SendEvent { event, timestamp }; handle.event_bus().send(event_frame).await; Ok(()) } else { @@ -122,6 +171,14 @@ impl GameManager { } } + pub async fn get_checkpoint(&self, game_addr: &str, settle_version: u64) -> Result> { + let games = self.games.lock().await; + let handle = games.get(game_addr).ok_or(Error::GameNotLoaded)?; + let broadcaster = handle.broadcaster()?; + let checkpoint = broadcaster.get_checkpoint(settle_version).await; + Ok(checkpoint) + } + /// Get the broadcast channel of game, and its event histories pub async fn get_broadcast( &self, @@ -135,4 +192,11 @@ impl GameManager { let histories = broadcaster.retrieve_histories(settle_version).await; Ok((receiver, histories)) } + + pub async fn get_event_parent(&self, game_addr: &str) -> Result { + let games = self.games.lock().await; + let handle = games.get(game_addr).ok_or(Error::GameNotLoaded)?; + let bridge_parent = handle.event_parent_owned()?; + Ok(bridge_parent) + } } diff --git a/transactor/src/handle.rs b/transactor/src/handle.rs index ece7e9a3..e64f073f 100644 --- a/transactor/src/handle.rs +++ b/transactor/src/handle.rs @@ -1,193 +1,29 @@ +mod subgame; +mod transactor; +mod validator; + use std::sync::Arc; use crate::component::{ - Broadcaster, Component, EventBus, EventLoop, GameSynchronizer, LocalConnection, PortsHandle, - RemoteConnection, Submitter, Subscriber, Voter, WrappedClient, WrappedHandler, WrappedTransport, CloseReason, + Broadcaster, CloseReason, EventBridgeParent, EventBus, WrappedStorage, WrappedTransport, }; -use crate::frame::EventFrame; -use race_core::context::GameContext; +use crate::frame::SignalFrame; use race_api::error::{Error, Result}; +use race_core::storage::StorageT; use race_core::transport::TransportT; -use race_core::types::{ClientMode, GameAccount, GameBundle, ServerAccount, QueryMode}; +use race_core::types::{GetCheckpointParams, QueryMode, ServerAccount, SubGameSpec}; use race_encryptor::Encryptor; +use subgame::SubGameHandle; +use tokio::sync::mpsc; use tokio::task::JoinHandle; use tracing::info; +use transactor::TransactorHandle; +use validator::ValidatorHandle; pub enum Handle { Transactor(TransactorHandle), Validator(ValidatorHandle), -} - -#[allow(dead_code)] -pub struct TransactorHandle { - addr: String, - handles: Vec, - event_bus: EventBus, - broadcaster: Broadcaster, -} - -impl TransactorHandle { - pub async fn try_new( - game_account: &GameAccount, - server_account: &ServerAccount, - bundle_account: &GameBundle, - encryptor: Arc, - transport: Arc, - ) -> Result { - info!( - "Start game handle for {} with Transactor mode", - game_account.addr - ); - - let game_context = GameContext::try_new(game_account)?; - let handler = WrappedHandler::load_by_bundle(bundle_account, encryptor.clone()).await?; - - let event_bus = EventBus::default(); - - let (broadcaster, broadcaster_ctx) = Broadcaster::init(game_account); - let mut broadcaster_handle = broadcaster.start(broadcaster_ctx); - - let (event_loop, event_loop_ctx) = - EventLoop::init(handler, game_context, ClientMode::Transactor); - let mut event_loop_handle = event_loop.start(event_loop_ctx); - - let (submitter, submitter_ctx) = Submitter::init(game_account, transport.clone()); - let mut submitter_handle = submitter.start(submitter_ctx); - - let (synchronizer, synchronizer_ctx) = - GameSynchronizer::init(transport.clone(), game_account); - - let mut connection = LocalConnection::new(encryptor.clone()); - - event_bus.attach(&mut connection).await; - let (client, client_ctx) = WrappedClient::init( - server_account, - game_account, - transport.clone(), - encryptor, - Arc::new(connection), - ); - let mut client_handle = client.start(client_ctx); - - info!("Attaching components"); - event_bus.attach(&mut broadcaster_handle).await; - event_bus.attach(&mut submitter_handle).await; - event_bus.attach(&mut event_loop_handle).await; - event_bus.attach(&mut client_handle).await; - - // Dispatch init state - let init_account = game_account.derive_init_account(); - info!("InitAccount: {:?}", init_account); - event_bus - .send(EventFrame::InitState {init_account}) - .await; - - let mut synchronizer_handle = synchronizer.start(synchronizer_ctx); - event_bus.attach(&mut synchronizer_handle).await; - - Ok(Self { - addr: game_account.addr.clone(), - event_bus, - handles: vec![ - broadcaster_handle, - submitter_handle, - event_loop_handle, - client_handle, - synchronizer_handle, - ], - broadcaster, - }) - } -} - -#[allow(dead_code)] -pub struct ValidatorHandle { - addr: String, - event_bus: EventBus, - handles: Vec, -} - -impl ValidatorHandle { - pub async fn try_new( - game_account: &GameAccount, - server_account: &ServerAccount, - bundle_account: &GameBundle, - encryptor: Arc, - transport: Arc, - ) -> Result { - info!( - "Start game handle for {} with Validator mode", - game_account.addr - ); - let game_context = GameContext::try_new(game_account)?; - let handler = WrappedHandler::load_by_bundle(bundle_account, encryptor.clone()).await?; - - let transactor_addr = game_account - .transactor_addr - .as_ref() - .ok_or(Error::GameNotServed)?; - let transactor_account = transport - .get_server_account(transactor_addr) - .await? - .ok_or(Error::CantFindTransactor)?; - - info!("Creating components"); - let event_bus = EventBus::default(); - - let (event_loop, event_loop_ctx) = - EventLoop::init(handler, game_context, ClientMode::Validator); - let mut event_loop_handle = event_loop.start(event_loop_ctx); - - let connection = Arc::new( - RemoteConnection::try_new( - &server_account.addr, - &transactor_account.endpoint, - encryptor.clone(), - ) - .await?, - ); - // let mut subscriber = Subscriber::new(game_account, server_account, connection.clone()); - let (subscriber, subscriber_context) = - Subscriber::init(game_account, server_account, connection.clone()); - let mut subscriber_handle = subscriber.start(subscriber_context); - - let (client, client_ctx) = WrappedClient::init( - server_account, - game_account, - transport.clone(), - encryptor, - connection, - ); - let mut client_handle = client.start(client_ctx); - - let (voter, voter_ctx) = Voter::init(game_account, server_account, transport.clone()); - let mut voter_handle = voter.start(voter_ctx); - - info!("Attaching components"); - event_bus.attach(&mut event_loop_handle).await; - event_bus.attach(&mut voter_handle).await; - event_bus.attach(&mut client_handle).await; - - let init_account = game_account.derive_rollbacked_init_account(); - info!("InitAccount: {:?}", init_account); - - // Dispatch init state - event_bus - .send(EventFrame::InitState {init_account}) - .await; - - event_bus.attach(&mut subscriber_handle).await; - Ok(Self { - addr: game_account.addr.clone(), - event_bus, - handles: vec![ - subscriber_handle, - client_handle, - event_loop_handle, - voter_handle, - ], - }) - } + SubGame(SubGameHandle), } /// The handle to the components set of a game. @@ -202,17 +38,27 @@ impl Handle { /// Create game handle. pub async fn try_new( transport: Arc, + storage: Arc, encryptor: Arc, server_account: &ServerAccount, addr: &str, + signal_tx: mpsc::Sender, + debug_mode: bool, ) -> Result { info!("Try create game handle for {}", addr); - let mode = QueryMode::Confirming; + let mode = QueryMode::Finalized; let game_account = transport .get_game_account(addr, mode) .await? .ok_or(Error::GameAccountNotFound)?; + let checkpoint_offchain = storage + .get_checkpoint(GetCheckpointParams { + game_addr: addr.to_owned(), + settle_version: game_account.settle_version, + }) + .await?; + if let Some(ref transactor_addr) = game_account.transactor_addr { info!("Current transactor: {}", transactor_addr); // Query the game bundle @@ -226,10 +72,14 @@ impl Handle { Ok(Self::Transactor( TransactorHandle::try_new( &game_account, + checkpoint_offchain, server_account, &game_bundle, encryptor.clone(), transport.clone(), + storage.clone(), + signal_tx, + debug_mode, ) .await?, )) @@ -237,10 +87,13 @@ impl Handle { Ok(Self::Validator( ValidatorHandle::try_new( &game_account, + checkpoint_offchain, server_account, &game_bundle, encryptor.clone(), transport.clone(), + signal_tx, + debug_mode, ) .await?, )) @@ -250,10 +103,39 @@ impl Handle { } } + pub async fn try_new_sub_game_handle( + spec: SubGameSpec, + bridge_parent: EventBridgeParent, + server_account: &ServerAccount, + encryptor: Arc, + transport: Arc, + debug_mode: bool, + ) -> Result { + let handle = SubGameHandle::try_new( + spec, + bridge_parent, + server_account, + encryptor, + transport, + debug_mode, + ) + .await?; + Ok(Self::SubGame(handle)) + } + pub fn broadcaster(&self) -> Result<&Broadcaster> { match self { Handle::Transactor(h) => Ok(&h.broadcaster), Handle::Validator(_) => Err(Error::NotSupportedInValidatorMode), + Handle::SubGame(h) => Ok(&h.broadcaster), + } + } + + pub fn event_parent_owned(&self) -> Result { + match self { + Handle::Transactor(h) => Ok(h.bridge_parent.to_owned()), + Handle::Validator(h) => Ok(h.bridge_parent.to_owned()), + Handle::SubGame(_) => Err(Error::NotSupportedInSubGameMode), } } @@ -261,6 +143,7 @@ impl Handle { match self { Handle::Transactor(h) => &h.event_bus, Handle::Validator(h) => &h.event_bus, + Handle::SubGame(h) => &h.event_bus, } } @@ -268,6 +151,7 @@ impl Handle { let handles = match self { Handle::Transactor(ref mut x) => &mut x.handles, Handle::Validator(ref mut x) => &mut x.handles, + Handle::SubGame(ref mut x) => &mut x.handles, }; if handles.is_empty() { panic!("Some where else is waiting"); diff --git a/transactor/src/handle/subgame.rs b/transactor/src/handle/subgame.rs new file mode 100644 index 00000000..b5551de1 --- /dev/null +++ b/transactor/src/handle/subgame.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; + +use crate::component::{ + Broadcaster, Component, EventBridgeChild, EventBridgeParent, EventBus, EventLoop, + LocalConnection, PortsHandle, WrappedClient, WrappedHandler, +}; +use crate::frame::EventFrame; +use race_api::error::{Error, Result}; +use race_core::context::GameContext; +use race_core::transport::TransportT; +use race_core::types::{ClientMode, GameMode, ServerAccount, SubGameSpec}; +use race_encryptor::Encryptor; + +#[allow(dead_code)] +pub struct SubGameHandle { + pub(crate) addr: String, + pub(crate) event_bus: EventBus, + pub(crate) handles: Vec, + pub(crate) broadcaster: Broadcaster, + pub(crate) bridge_child: EventBridgeChild, +} + +impl SubGameHandle { + pub async fn try_new( + spec: SubGameSpec, + bridge_parent: EventBridgeParent, + server_account: &ServerAccount, + encryptor: Arc, + transport: Arc, + debug_mode: bool, + ) -> Result { + let game_addr = spec.game_addr.clone(); + let game_id = spec.game_id.clone(); + let addr = format!("{}:{}", game_addr, game_id); + let event_bus = EventBus::new(addr.to_string()); + + let bundle_account = transport + .get_game_bundle(&spec.bundle_addr) + .await? + .ok_or(Error::GameBundleNotFound)?; + + // Build an InitAccount + let game_context = GameContext::try_new_with_sub_game_spec(&spec)?; + + let access_version = spec.access_version; + let settle_version = spec.settle_version; + + let handler = WrappedHandler::load_by_bundle(&bundle_account, encryptor.clone()).await?; + + let (broadcaster, broadcaster_ctx) = Broadcaster::init(addr.clone(), game_id, debug_mode); + let mut broadcaster_handle = broadcaster.start(&addr, broadcaster_ctx); + + let (bridge, bridge_ctx) = bridge_parent.derive_child(game_id.clone()); + let mut bridge_handle = bridge.start(&addr, bridge_ctx); + + let (event_loop, event_loop_ctx) = + EventLoop::init(handler, game_context, ClientMode::Transactor, GameMode::Sub); + let mut event_loop_handle = event_loop.start(&addr, event_loop_ctx); + + let mut connection = LocalConnection::new(encryptor.clone()); + + event_bus.attach(&mut connection).await; + + let (client, client_ctx) = WrappedClient::init( + server_account.addr.clone(), + addr.clone(), + ClientMode::Transactor, + transport.clone(), + encryptor, + Arc::new(connection), + ); + let mut client_handle = client.start(&addr, client_ctx); + + event_bus.attach(&mut client_handle).await; + event_bus.attach(&mut bridge_handle).await; + event_bus.attach(&mut broadcaster_handle).await; + event_bus.attach(&mut event_loop_handle).await; + event_bus + .send(EventFrame::InitState { + init_account: spec.init_account, + access_version, + settle_version, + }) + .await; + + Ok(Self { + addr: format!("{}:{}", game_addr, game_id), + event_bus, + handles: vec![broadcaster_handle, bridge_handle, event_loop_handle], + broadcaster, + bridge_child: bridge, + }) + } +} diff --git a/transactor/src/handle/transactor.rs b/transactor/src/handle/transactor.rs new file mode 100644 index 00000000..8cca9911 --- /dev/null +++ b/transactor/src/handle/transactor.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use crate::component::{ + Broadcaster, Component, EventBridgeParent, EventBus, EventLoop, GameSynchronizer, + LocalConnection, PortsHandle, Submitter, WrappedClient, WrappedHandler, +}; +use crate::frame::{EventFrame, SignalFrame}; +use race_api::error::{Error, Result}; +use race_api::types::{PlayerJoin, ServerJoin}; +use race_core::checkpoint::CheckpointOffChain; +use race_core::context::GameContext; +use race_core::storage::StorageT; +use race_core::transport::TransportT; +use race_core::types::{ClientMode, GameAccount, GameBundle, GameMode, ServerAccount}; +use race_encryptor::Encryptor; +use tokio::sync::mpsc; +use tracing::info; + +#[allow(dead_code)] +pub struct TransactorHandle { + pub(crate) addr: String, + pub(crate) handles: Vec, + pub(crate) event_bus: EventBus, + pub(crate) broadcaster: Broadcaster, + pub(crate) bridge_parent: EventBridgeParent, +} + +fn create_init_sync(game_account: &GameAccount) -> Result { + let checkpoint_access_version = game_account + .checkpoint_on_chain + .as_ref() + .map(|cp| cp.access_version) + .unwrap_or_default(); + + let new_players: Vec = game_account + .players + .iter() + .filter(|p| p.access_version > checkpoint_access_version) + .cloned() + .collect(); + + let new_servers: Vec = game_account + .servers + .iter() + .filter(|s| s.access_version > checkpoint_access_version) + .cloned() + .collect(); + + let transactor_addr = game_account + .transactor_addr + .clone() + .ok_or(Error::GameNotServed)?; + + let init_sync = EventFrame::Sync { + access_version: game_account.access_version, + new_players, + new_servers, + transactor_addr, + }; + + Ok(init_sync) +} + +impl TransactorHandle { + pub async fn try_new( + game_account: &GameAccount, + checkpoint_off_chain: Option, + server_account: &ServerAccount, + bundle_account: &GameBundle, + encryptor: Arc, + transport: Arc, + storage: Arc, + signal_tx: mpsc::Sender, + debug_mode: bool, + ) -> Result { + info!( + "Start game handle for {} with Transactor mode", + game_account.addr + ); + + let game_context = GameContext::try_new(game_account, checkpoint_off_chain)?; + let init_account = game_context.init_account()?; + + let handler = WrappedHandler::load_by_bundle(bundle_account, encryptor.clone()).await?; + + let event_bus = EventBus::new(game_account.addr.clone()); + + let (broadcaster, broadcaster_ctx) = + Broadcaster::init(game_account.addr.clone(), 0, debug_mode); + let mut broadcaster_handle = broadcaster.start(&game_account.addr, broadcaster_ctx); + + let (bridge, bridge_ctx) = EventBridgeParent::init(signal_tx); + let mut bridge_handle = bridge.start(&game_account.addr, bridge_ctx); + + let (event_loop, event_loop_ctx) = EventLoop::init( + handler, + game_context, + ClientMode::Transactor, + GameMode::Main, + ); + let mut event_loop_handle = event_loop.start(&game_account.addr, event_loop_ctx); + + let (submitter, submitter_ctx) = + Submitter::init(game_account, transport.clone(), storage.clone()); + let mut submitter_handle = submitter.start(&game_account.addr, submitter_ctx); + + let (synchronizer, synchronizer_ctx) = + GameSynchronizer::init(transport.clone(), game_account); + + let mut connection = LocalConnection::new(encryptor.clone()); + + event_bus.attach(&mut connection).await; + let (client, client_ctx) = WrappedClient::init( + server_account.addr.clone(), + game_account.addr.clone(), + ClientMode::Transactor, + transport.clone(), + encryptor, + Arc::new(connection), + ); + let mut client_handle = client.start(&game_account.addr, client_ctx); + + event_bus.attach(&mut broadcaster_handle).await; + event_bus.attach(&mut bridge_handle).await; + event_bus.attach(&mut submitter_handle).await; + event_bus.attach(&mut event_loop_handle).await; + event_bus.attach(&mut client_handle).await; + + // Dispatch init state + // let init_account = game_account.derive_checkpoint_init_account(); + info!("InitAccount: {:?}", init_account); + event_bus + .send(EventFrame::InitState { + init_account, + access_version: game_account.access_version, + settle_version: game_account.settle_version, + }) + .await; + let init_sync = create_init_sync(game_account)?; + info!("Dispatch init sync: {:?}", init_sync); + event_bus.send(init_sync).await; + + let mut synchronizer_handle = synchronizer.start(&game_account.addr, synchronizer_ctx); + event_bus.attach(&mut synchronizer_handle).await; + + Ok(Self { + addr: game_account.addr.clone(), + event_bus, + handles: vec![ + broadcaster_handle, + submitter_handle, + event_loop_handle, + client_handle, + synchronizer_handle, + ], + broadcaster, + bridge_parent: bridge, + }) + } +} diff --git a/transactor/src/handle/validator.rs b/transactor/src/handle/validator.rs new file mode 100644 index 00000000..297d2f1e --- /dev/null +++ b/transactor/src/handle/validator.rs @@ -0,0 +1,119 @@ +use std::sync::Arc; + +use crate::component::{ + Component, EventBridgeParent, EventBus, EventLoop, PortsHandle, RemoteConnection, Subscriber, + Voter, WrappedClient, WrappedHandler, WrappedTransport, +}; +use crate::frame::{EventFrame, SignalFrame}; +use race_api::error::{Error, Result}; +use race_core::checkpoint::CheckpointOffChain; +use race_core::context::GameContext; +use race_core::transport::TransportT; +use race_core::types::{ClientMode, GameAccount, GameBundle, ServerAccount, GameMode}; +use race_encryptor::Encryptor; +use tokio::sync::mpsc; +use tracing::info; + +#[allow(dead_code)] +pub struct ValidatorHandle { + pub(crate) addr: String, + pub(crate) event_bus: EventBus, + pub(crate) handles: Vec, + pub(crate) bridge_parent: EventBridgeParent, +} + +impl ValidatorHandle { + pub async fn try_new( + game_account: &GameAccount, + checkpoint_off_chain: Option, + server_account: &ServerAccount, + bundle_account: &GameBundle, + encryptor: Arc, + transport: Arc, + signal_tx: mpsc::Sender, + _debug_mode: bool, + ) -> Result { + info!( + "Start game handle for {} with Validator mode", + game_account.addr + ); + let game_context = GameContext::try_new(game_account, checkpoint_off_chain)?; + let init_account = game_context.init_account()?; + + let handler = WrappedHandler::load_by_bundle(bundle_account, encryptor.clone()).await?; + + let transactor_addr = game_account + .transactor_addr + .as_ref() + .ok_or(Error::GameNotServed)?; + let transactor_account = transport + .get_server_account(transactor_addr) + .await? + .ok_or(Error::CantFindTransactor)?; + + info!("Creating components"); + let event_bus = EventBus::new(game_account.addr.clone()); + + let (bridge, bridge_ctx) = EventBridgeParent::init(signal_tx); + let mut bridge_handle = bridge.start(&game_account.addr, bridge_ctx); + + let (event_loop, event_loop_ctx) = + EventLoop::init(handler, game_context, ClientMode::Validator, GameMode::Main); + let mut event_loop_handle = event_loop.start(&game_account.addr, event_loop_ctx); + + let connection = Arc::new( + RemoteConnection::try_new( + &server_account.addr, + &transactor_account.endpoint, + encryptor.clone(), + ) + .await?, + ); + let (subscriber, subscriber_context) = + Subscriber::init(game_account, server_account, connection.clone()); + let mut subscriber_handle = subscriber.start(&game_account.addr, subscriber_context); + + let (client, client_ctx) = WrappedClient::init( + server_account.addr.clone(), + game_account.addr.clone(), + ClientMode::Validator, + transport.clone(), + encryptor, + connection, + ); + let mut client_handle = client.start(&game_account.addr, client_ctx); + + let (voter, voter_ctx) = Voter::init(game_account, server_account, transport.clone()); + let mut voter_handle = voter.start(&game_account.addr, voter_ctx); + + event_bus.attach(&mut bridge_handle).await; + event_bus.attach(&mut event_loop_handle).await; + event_bus.attach(&mut voter_handle).await; + event_bus.attach(&mut client_handle).await; + + // let init_account = game_account.derive_checkpoint_init_account(); + info!("InitAccount: {:?}", init_account); + + // Dispatch init state + event_bus + .send(EventFrame::InitState { + init_account, + access_version: game_account.access_version, + settle_version: game_account.settle_version, + }) + .await; + + event_bus.attach(&mut subscriber_handle).await; + Ok(Self { + addr: game_account.addr.clone(), + event_bus, + handles: vec![ + subscriber_handle, + client_handle, + event_loop_handle, + voter_handle, + ], + bridge_parent: bridge, + }) + } +} diff --git a/transactor/src/main.rs b/transactor/src/main.rs index b7839b03..60be85fb 100644 --- a/transactor/src/main.rs +++ b/transactor/src/main.rs @@ -13,6 +13,7 @@ use clap::{arg, Command}; use context::ApplicationContext; use race_env::Config; use reg::{register_server, start_reg_task}; +use tracing_subscriber::{fmt, prelude::__tracing_subscriber_SubscriberExt, Layer, EnvFilter}; fn cli() -> Command { Command::new("transactor") @@ -24,13 +25,32 @@ fn cli() -> Command { .subcommand(Command::new("reg").about("Register server account")) } +fn setup_logger() { + let logfile = tracing_appender::rolling::daily("logs", "transactor.log"); + let file_layer = fmt::layer() + .with_writer(logfile) + .with_target(true) + .with_level(true) + .with_ansi(false) + .with_filter(EnvFilter::from_default_env()); + let console_layer = fmt::layer() + .compact() + .with_writer(std::io::stdout) + .with_level(true) + .with_ansi(true) + .without_time() + .with_filter(EnvFilter::from_default_env()); + let subscriber = tracing_subscriber::registry() + .with(console_layer) + .with(file_layer); + + tracing::subscriber::set_global_default(subscriber).expect("Failed to configure logger"); +} + #[tokio::main] pub async fn main() { - let log_format = tracing_subscriber::fmt::format() - .with_level(true) - .with_target(false) - .compact(); - tracing_subscriber::fmt().event_format(log_format).init(); + + setup_logger(); let matches = cli().get_matches(); let config = Config::from_path(&matches.get_one::("config").unwrap().into()).await; diff --git a/transactor/src/server.rs b/transactor/src/server.rs index 14b60dbd..991e6824 100644 --- a/transactor/src/server.rs +++ b/transactor/src/server.rs @@ -3,19 +3,20 @@ use std::{net::SocketAddr, sync::Arc}; use crate::context::ApplicationContext; use crate::utils; use borsh::BorshDeserialize; -use borsh::BorshSerialize; use hyper::Method; use jsonrpsee::core::error::Error as RpcError; -use jsonrpsee::core::error::SubscriptionClosed; +use jsonrpsee::core::StringError; use jsonrpsee::server::AllowHosts; use jsonrpsee::types::error::CallError; -use jsonrpsee::types::SubscriptionEmptyError; -use jsonrpsee::SubscriptionSink; +use jsonrpsee::types::ErrorObjectOwned; use jsonrpsee::{server::ServerBuilder, types::Params, RpcModule}; +use jsonrpsee::{PendingSubscriptionSink, SubscriptionMessage, TrySendError}; use race_api::event::Message; +use race_core::checkpoint::CheckpointOffChain; use race_core::types::SubmitMessageParams; use race_core::types::{ - AttachGameParams, ExitGameParams, Signature, SubmitEventParams, SubscribeEventParams, + AttachGameParams, CheckpointParams, ExitGameParams, Signature, SubmitEventParams, + SubscribeEventParams, }; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::StreamExt; @@ -60,15 +61,24 @@ fn parse_params( } /// Ask transactor to load game and provide client's public key for further encryption. -async fn attach_game(params: Params<'_>, context: Arc) -> Result<(), RpcError> { - let (_game_addr, AttachGameParams { signer, key }) = parse_params_no_sig(params)?; +async fn attach_game( + params: Params<'_>, + context: Arc, +) -> Result<(), RpcError> { + let (game_addr, AttachGameParams { signer, key }) = parse_params_no_sig(params)?; info!("Attach to game, signer: {}", signer); + if !context.game_manager.is_game_loaded(&game_addr).await { + return Err(RpcError::Custom("Game not loaded".to_string())); + } + context .register_key(signer, key) .await - .map_err(|e| RpcError::Call(CallError::Failed(e.into()))) + .map_err(|e| RpcError::Call(CallError::Failed(e.into())))?; + + Ok(()) } fn ping(_: Params<'_>, _: &ApplicationContext) -> Result { @@ -82,6 +92,7 @@ async fn submit_message( let (game_addr, SubmitMessageParams { content }, sig) = parse_params(params, &context)?; let sender = sig.signer; + info!("Player message, {}: {}", sender, content); let message = Message { content, sender }; context @@ -104,6 +115,27 @@ async fn submit_event( .map_err(|e| RpcError::Call(CallError::Failed(e.into()))) } +async fn get_checkpoint( + params: Params<'_>, + context: Arc, +) -> Result>, RpcError> { + let (game_addr, CheckpointParams { settle_version }) = parse_params_no_sig(params)?; + + info!("Get checkpoint, game_addr: {}", game_addr); + + let checkpoint: Option = context + .game_manager + .get_checkpoint(&game_addr, settle_version) + .await + .map_err(|e| RpcError::Call(CallError::Failed(e.into())))?; + + let bs = checkpoint + .map(|c| borsh::to_vec(&c).map_err(|e| RpcError::Call(CallError::Failed(e.into())))) + .transpose()?; + + Ok(bs) +} + async fn exit_game(params: Params<'_>, context: Arc) -> Result<(), RpcError> { let (game_addr, ExitGameParams {}, sig) = parse_params(params, &context)?; info!("Exit game"); @@ -114,72 +146,81 @@ async fn exit_game(params: Params<'_>, context: Arc) -> Resu .map_err(|e| RpcError::Call(CallError::Failed(e.into()))) } -fn subscribe_event( +async fn subscribe_event( params: Params<'_>, - mut sink: SubscriptionSink, + pending: PendingSubscriptionSink, context: Arc, -) -> Result<(), SubscriptionEmptyError> { +) -> Result<(), StringError> { { - let (game_addr, SubscribeEventParams { settle_version }) = - parse_params_no_sig(params).or(Err(SubscriptionEmptyError))?; - - tokio::spawn(async move { - let (receiver, histories) = - match context.get_broadcast(&game_addr, settle_version).await { - Ok(x) => x, - Err(e) => { - sink.close(SubscriptionClosed::Failed( - CallError::Failed(e.into()).into(), - )); - return; - } - }; - - drop(context); - - info!( - "Subscribe event stream, game: {:?}, settle version: {}, number of histories: {}", - game_addr, - settle_version, - histories.len() - ); - histories.into_iter().for_each(|x| { - // info!("Push history event: {}", x); - let v = x.try_to_vec().unwrap(); + let (game_addr, SubscribeEventParams { settle_version }) = match parse_params_no_sig(params) + { + Ok(p) => p, + Err(e) => { + let _ = pending.reject(ErrorObjectOwned::from(e)).await; + return Ok(()); + } + }; + + let (receiver, frames) = match context.get_broadcast(&game_addr, settle_version).await { + Ok(x) => x, + Err(e) => { + warn!("Game not found: {}", game_addr); + let _ = pending.reject(CallError::Failed(e.into())).await; + return Ok(()); + } + }; + + drop(context); + info!( + "Subscribe event stream, game: {:?}, settle version: {}, number of historical frames: {}", + game_addr, + settle_version, + frames.len() + ); + + let mut sink = pending.accept().await?; + + for frame in frames.into_iter() { + let v = borsh::to_vec(&frame).unwrap(); + let s = utils::base64_encode(&v); + sink.send(SubscriptionMessage::from(&s)) + .await + .map_err(|e| { + error!("Error occurred when broadcasting historical frame: {:?}", e); + e + }) + .unwrap(); + } + + let rx = BroadcastStream::new(receiver); + let mut serialized_rx = rx.map(|f| match f { + Ok(x) => { + let v = borsh::to_vec(&x).unwrap(); let s = utils::base64_encode(&v); - sink.send(&s) - .map_err(|e| { - error!("Error occurred when broadcasting event histories: {:?}", e); - e - }) - .unwrap(); - }); - - let rx = BroadcastStream::new(receiver); - let serialized_rx = rx.map(|f| match f { - Ok(x) => { - let v = x.try_to_vec().unwrap(); - let s = utils::base64_encode(&v); - // info!("Push new event: {}", x); - Ok(s) - } - Err(e) => Err(e), - }); - - match sink.pipe_from_try_stream(serialized_rx).await { - SubscriptionClosed::Success => { - info!("Subscription closed successfully"); - sink.close(SubscriptionClosed::Success); - } - SubscriptionClosed::RemotePeerAborted => { - warn!("Remote peer aborted"); - } - SubscriptionClosed::Failed(err) => { - warn!("Subscription error: {:?}", err); - sink.close(err); - } - }; + Ok(s) + } + Err(e) => Err(e), }); + + loop { + tokio::select! { + _ = sink.closed() => break Err(anyhow::anyhow!("Subscription was closed")), + maybe_item = serialized_rx.next() => { + let item = match maybe_item { + Some(Ok(item)) => item, + _ => break Err(anyhow::anyhow!("Event stream ended")), + }; + let msg = SubscriptionMessage::from(&item); + match sink.try_send(msg) { + Ok(_) => (), + Err(TrySendError::Closed(_)) => break Err(anyhow::anyhow!("Client disconnected, subscription closed")), + Err(TrySendError::Full(_)) => { + warn!("TrySendError::Full"); + } + } + }, + } + }?; Ok(()) } } @@ -206,6 +247,7 @@ pub async fn run_server(context: ApplicationContext) -> anyhow::Result<()> { let mut module = RpcModule::new(context); module.register_method("ping", ping)?; + module.register_async_method("checkpoint", get_checkpoint)?; module.register_async_method("attach_game", attach_game)?; module.register_async_method("submit_event", submit_event)?; module.register_async_method("submit_message", submit_message)?; diff --git a/transactor/src/utils.rs b/transactor/src/utils.rs index 141946a9..733c8936 100644 --- a/transactor/src/utils.rs +++ b/transactor/src/utils.rs @@ -1,4 +1,5 @@ use base64::Engine; +use std::time::UNIX_EPOCH; pub fn base64_encode(data: &[u8]) -> String { let engine = base64::engine::general_purpose::STANDARD; @@ -13,5 +14,17 @@ pub fn base64_decode(data: &str) -> Result, race_api::error::Error> { } pub fn addr_shorthand(addr: &str) -> String { - format!("[{}]", &addr[0..3]) + if addr.len() > 6 { + let l = addr.len(); + addr[(l - 6)..l].to_string() + } else { + addr.to_string() + } +} + +pub fn current_timestamp() -> u64 { + std::time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 } diff --git a/transport/Cargo.toml b/transport/Cargo.toml index 3855a2aa..0a78f475 100644 --- a/transport/Cargo.toml +++ b/transport/Cargo.toml @@ -16,20 +16,24 @@ publish = true [dependencies] race-api = { workspace = true, features = ["serde"] } race-core = { workspace = true, features = ["serde"] } -race-env = { workspace = true } -async-trait = { workspace = true } -thiserror = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -tracing = { workspace = true } -borsh = { workspace = true } +race-env.workspace = true +async-trait.workspace = true +thiserror.workspace = true +serde_json.workspace = true +serde.workspace = true +tracing.workspace = true +borsh.workspace = true jsonrpsee = { workspace = true, features = ["http-client"]} -solana-client = { workspace = true } -solana-sdk = { workspace = true } -spl-token = { workspace = true } -spl-associated-token-account = { workspace = true } +async-stream.workspace = true +futures.workspace = true +solana-account-decoder.workspace = true +solana-rpc-client.workspace = true +solana-rpc-client-api.workspace = true +solana-pubsub-client.workspace = true +solana-sdk.workspace = true +spl-token.workspace = true +spl-associated-token-account.workspace = true reqwest = { workspace = true, features = [ "json"] } -mpl-token-metadata = { workspace = true } [dev-dependencies] anyhow = { workspace = true } diff --git a/transport/src/error.rs b/transport/src/error.rs index 14375520..ee743c57 100644 --- a/transport/src/error.rs +++ b/transport/src/error.rs @@ -143,9 +143,14 @@ pub enum TransportError { #[error("Metadta Symbole too long (no more than 10 letters)")] InvalidMetadataSymbolLength, - #[error("Invalid settle address")] - InvalidSettleAddress, + #[error("Invalid settle address: {0}")] + InvalidSettleAddress(String), + #[error("Fee calculation error: {0}")] + FeeCalculationError(String), + + #[error("Subscription error: {0}")] + SubscriptionError(String), } pub type TransportResult = std::result::Result; diff --git a/transport/src/evm.rs b/transport/src/evm.rs index 8b96174a..a80dd433 100644 --- a/transport/src/evm.rs +++ b/transport/src/evm.rs @@ -1,5 +1,3 @@ -// use web3::{transports::Http, Web3}; - use race_core::{ error::Result, transport::TransportT, diff --git a/transport/src/facade.rs b/transport/src/facade.rs index f017f2c5..90199b30 100644 --- a/transport/src/facade.rs +++ b/transport/src/facade.rs @@ -1,6 +1,11 @@ +use std::pin::Pin; + +use async_stream::stream; use async_trait::async_trait; use borsh::BorshDeserialize; +use futures::Stream; use jsonrpsee::core::client::ClientT; +use jsonrpsee::http_client::transport::HttpBackend; use jsonrpsee::rpc_params; use jsonrpsee::http_client::{HttpClient as Client, HttpClientBuilder as ClientBuilder}; @@ -10,10 +15,12 @@ use race_api::error::{Error, Result}; use race_core::transport::TransportT; use race_core::types::{ - CloseGameAccountParams, CreateGameAccountParams, CreatePlayerProfileParams, - CreateRegistrationParams, DepositParams, GameAccount, GameBundle, JoinParams, PlayerProfile, - PublishGameParams, RegisterGameParams, RegisterServerParams, RegistrationAccount, ServeParams, - ServerAccount, SettleParams, UnregisterGameParams, VoteParams, CreateRecipientParams, AssignRecipientParams, RecipientAccount,QueryMode, RecipientClaimParams + AssignRecipientParams, CloseGameAccountParams, CreateGameAccountParams, + CreatePlayerProfileParams, CreateRecipientParams, CreateRegistrationParams, DepositParams, + GameAccount, GameBundle, JoinParams, PlayerProfile, PublishGameParams, QueryMode, + RecipientAccount, RecipientClaimParams, RegisterGameParams, RegisterServerParams, + RegistrationAccount, ServeParams, ServerAccount, SettleParams, UnregisterGameParams, + VoteParams, }; use serde::Serialize; @@ -36,15 +43,16 @@ pub struct RegisterServerInstruction { pub struct FacadeTransport { addr: String, - client: Client, + client: Client, } impl FacadeTransport { pub async fn try_new(addr: String, url: &str) -> TransportResult { let client = ClientBuilder::default() - .max_request_body_size(64_000_000) + .max_request_size(64_000_000) .build(url) .map_err(|e| TransportError::InitializationFailed(e.to_string()))?; + Ok(Self { addr, client }) } @@ -128,10 +136,32 @@ impl TransportT for FacadeTransport { } } + async fn subscribe_game_account<'a>( + &'a self, + addr: &'a str, + ) -> Result> + Send + 'a>>> { + Ok(Box::pin(stream! { + let mut access_version = 0; + loop { + match self.fetch::("get_account_info", addr).await { + Ok(game_account_opt) => { + if let Some(game_account) = game_account_opt { + if game_account.access_version > access_version { + access_version = game_account.access_version; + yield Some(game_account); + } + } + } + Err(e) => yield None, + } + } + })) + } + async fn get_game_account(&self, addr: &str, mode: QueryMode) -> Result> { match mode { - QueryMode::Confirming => {}, - QueryMode::Finalized => {}, + QueryMode::Confirming => {} + QueryMode::Finalized => {} } self.fetch("get_account_info", addr).await } @@ -164,7 +194,7 @@ impl TransportT for FacadeTransport { unimplemented!() } - async fn settle_game(&self, params: SettleParams) -> Result<()> { + async fn settle_game(&self, params: SettleParams) -> Result { self.client .request("settle", rpc_params![params]) .await diff --git a/transport/src/solana.rs b/transport/src/solana.rs index d1ae9d50..1369c305 100755 --- a/transport/src/solana.rs +++ b/transport/src/solana.rs @@ -1,7 +1,10 @@ mod constants; +mod metadata; mod types; +use async_stream::stream; use constants::*; +use futures::{Stream, StreamExt}; use tracing::{error, info}; use types::*; @@ -22,12 +25,12 @@ use race_core::{ }; // use core::slice::SlicePattern; -use std::path::PathBuf; use std::str::FromStr; +use std::{path::PathBuf, pin::Pin}; -use mpl_token_metadata as metaplex_program; -use mpl_token_metadata::state::Metadata; -use solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig}; +use solana_pubsub_client::nonblocking::pubsub_client::PubsubClient; +use solana_rpc_client::rpc_client::RpcClient; +use solana_rpc_client_api::config::{RpcAccountInfoConfig, RpcSendTransactionConfig}; use solana_sdk::compute_budget::ComputeBudgetInstruction; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; @@ -44,6 +47,7 @@ use solana_sdk::{ system_program, sysvar::rent, }; +use solana_account_decoder::UiAccountEncoding; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; @@ -55,6 +59,8 @@ use spl_token::{ mod nft; +const METAPLEX_PROGRAM_ID: &str = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"; + fn read_keypair(path: PathBuf) -> TransportResult { let keypair = solana_sdk::signature::read_keypair_file(path) .map_err(|e| TransportError::InvalidKeyfile(e.to_string()))?; @@ -66,11 +72,12 @@ fn player_addr_to_postition(game_state: &GameState, addr: &Pubkey) -> Result { - // return Err(TransportError::DuplicateServerAccount)?; - // } - // _ => {} - // } - let get_server_account_ix = create_account_with_seed( &payer_pubkey, &server_account_pubkey, @@ -227,8 +239,11 @@ impl TransportT for SolanaTransport { ], ); + let fee = self.get_recent_prioritization_fees(&[server_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + let message = Message::new( - &[get_server_account_ix, init_or_update_ix], + &[set_cu_prize_ix, get_server_account_ix, init_or_update_ix], Some(&payer_pubkey), ); @@ -360,7 +375,10 @@ impl TransportT for SolanaTransport { ], ); - let message = Message::new(&[serve_game_ix], Some(&payer_pubkey)); + let fee = self.get_recent_prioritization_fees(&[game_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + + let message = Message::new(&[set_cu_prize_ix, serve_game_ix], Some(&payer_pubkey)); let mut tx = Transaction::new_unsigned(message); let blockhash = self.get_blockhash()?; tx.sign(&[payer], blockhash); @@ -422,6 +440,10 @@ impl TransportT for SolanaTransport { ixs.push(init_profile_ix); + let fee = self.get_recent_prioritization_fees(&[profile_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + ixs.insert(0, set_cu_prize_ix); + let message = Message::new(&ixs, Some(&payer_pubkey)); let mut tx = Transaction::new_unsigned(message); @@ -434,6 +456,8 @@ impl TransportT for SolanaTransport { // TODO: add close_player_profile async fn publish_game(&self, params: PublishGameParams) -> Result { + let metaplex_program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID).unwrap(); + if params.name.len() > MAX_NAME_LENGTH { return Err(TransportError::InvalidMetadataNameLength)?; } @@ -468,20 +492,20 @@ impl TransportT for SolanaTransport { let (metadata_pda, _bump_seed) = Pubkey::find_program_address( &[ "metadata".as_bytes(), - metaplex_program::id().as_ref(), + metaplex_program_id.as_ref(), mint_pubkey.as_ref(), ], - &metaplex_program::id(), + &metaplex_program_id, ); let (edition_pda, _bump_seed) = Pubkey::find_program_address( &[ "metadata".as_bytes(), - metaplex_program::id().as_ref(), + metaplex_program_id.as_ref(), mint_pubkey.as_ref(), "edition".as_bytes(), ], - &metaplex_program::id(), + &metaplex_program_id, ); let ata_pubkey = get_associated_token_address(&payer_pubkey, &mint_pubkey); @@ -499,7 +523,7 @@ impl TransportT for SolanaTransport { AccountMeta::new(metadata_pda, false), AccountMeta::new(edition_pda, false), AccountMeta::new_readonly(spl_token::id(), false), - AccountMeta::new_readonly(metaplex_program::id(), false), + AccountMeta::new_readonly(metaplex_program_id, false), AccountMeta::new_readonly(rent::id(), false), AccountMeta::new_readonly(system_program::id(), false), ]; @@ -512,8 +536,12 @@ impl TransportT for SolanaTransport { accounts, ); + let fee = self.get_recent_prioritization_fees(&[mint_pubkey, metadata_pda, edition_pda])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + let message = Message::new( &[ + set_cu_prize_ix, create_mint_account_ix, init_mint_ix, create_ata_account_ix, @@ -530,7 +558,7 @@ impl TransportT for SolanaTransport { Ok(mint_pubkey.to_string()) } - async fn settle_game(&self, params: SettleParams) -> Result<()> { + async fn settle_game(&self, params: SettleParams) -> Result { let SettleParams { addr, mut settles, @@ -539,6 +567,7 @@ impl TransportT for SolanaTransport { settle_version, next_settle_version, } = params; + let payer = &self.keypair; let payer_pubkey = payer.pubkey(); let game_account_pubkey = Self::parse_pubkey(&addr)?; @@ -582,12 +611,16 @@ impl TransportT for SolanaTransport { ]; let mut ix_settles: Vec = Vec::new(); + let mut calc_cu_prize_addrs = + vec![Pubkey::from_str(&addr).unwrap(), game_state.stake_account]; + for settle in settles.iter() { match &settle.op { &SettleOp::Eject => { let addr = parse_pubkey(&settle.addr)?; let ata = get_associated_token_address(&addr, &game_state.token_mint); accounts.push(AccountMeta::new(ata, false)); + calc_cu_prize_addrs.push(ata); let position = player_addr_to_postition(&game_state, &addr)?; ix_settles.push(IxSettle { position, @@ -611,6 +644,7 @@ impl TransportT for SolanaTransport { for Transfer { slot_id, .. } in transfers.iter() { if let Some(slot) = recipient_state.slots.iter().find(|s| s.id == *slot_id) { accounts.push(AccountMeta::new(slot.stake_addr, false)); + calc_cu_prize_addrs.push(slot.stake_addr); } } @@ -627,23 +661,27 @@ impl TransportT for SolanaTransport { params: IxSettleParams { settles: ix_settles, transfers, - checkpoint, + checkpoint: borsh::to_vec(&checkpoint)?, settle_version, next_settle_version, }, }; let set_cu_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(1200000); + let fee = self.get_recent_prioritization_fees(&calc_cu_prize_addrs)?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); let settle_ix = Instruction::new_with_borsh(self.program_id, ¶ms, accounts); - let message = Message::new(&[set_cu_limit_ix, settle_ix], Some(&payer.pubkey())); + let message = Message::new( + &[set_cu_prize_ix, set_cu_limit_ix, settle_ix], + Some(&payer.pubkey()), + ); let mut tx = Transaction::new_unsigned(message); let blockhash = self.get_blockhash()?; tx.sign(&[payer], blockhash); - self.send_transaction(tx)?; - - Ok(()) + let sig = self.send_transaction(tx)?; + Ok(sig.to_string()) } async fn create_registration(&self, params: CreateRegistrationParams) -> Result { @@ -670,8 +708,11 @@ impl TransportT for SolanaTransport { ], ); + let fee = self.get_recent_prioritization_fees(&[registry_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + let message = Message::new( - &[create_account_ix, create_registry_ix], + &[set_cu_prize_ix, create_account_ix, create_registry_ix], Some(&payer.pubkey()), ); let blockhash = self.get_blockhash()?; @@ -827,7 +868,10 @@ impl TransportT for SolanaTransport { accounts, ); - let message = Message::new(&[register_game_ix], Some(&payer.pubkey())); + let fee = self.get_recent_prioritization_fees(&[reg_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + + let message = Message::new(&[set_cu_prize_ix, register_game_ix], Some(&payer.pubkey())); let mut tx = Transaction::new_unsigned(message); let blockhash = self.get_blockhash()?; tx.sign(&[payer], blockhash); @@ -855,7 +899,13 @@ impl TransportT for SolanaTransport { accounts, ); - let message = Message::new(&[unregister_game_ix], Some(&payer.pubkey())); + let fee = self.get_recent_prioritization_fees(&[reg_account_pubkey])?; + let set_cu_prize_ix = ComputeBudgetInstruction::set_compute_unit_price(fee); + + let message = Message::new( + &[set_cu_prize_ix, unregister_game_ix], + Some(&payer.pubkey()), + ); let mut tx = Transaction::new_unsigned(message); let blockhash = self .client @@ -867,26 +917,101 @@ impl TransportT for SolanaTransport { Ok(()) } + async fn subscribe_game_account<'a>( + &'a self, + addr: &'a str, + ) -> Result> + Send + 'a>>> { + let ws_rpc = self + .rpc + .replace("https://", "wss://") + .replace("http://", "ws://"); + let game_account_pubkey = Self::parse_pubkey(addr)?; + let addr = addr.to_owned(); + + Ok(Box::pin(stream! { + + let Ok(client) = PubsubClient::new(&ws_rpc).await else { + error!("Failed to create PubsubClient"); + return; + }; + + let Ok((mut stream, unsub)) = client + .account_subscribe( + &game_account_pubkey, + Some(RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + data_slice: None, + commitment: Some(CommitmentConfig::finalized()), + min_context_slot: None, + }), + ).await else { + error!("Failed on calling account_subscribe"); + return; + }; + + while let Some(rpc_response) = stream.next().await { + let ui_account = rpc_response.value; + let Some(data) = ui_account.data.decode() else { + error!("Found an empty account data"); + return; + }; + let state = match GameState::deserialize(&mut data.as_slice()) { + Ok(x) => x, + Err(e) => { + error!("Game state deserialization error: {}", e.to_string()); + unsub().await; + yield None; + break; + } + }; + let acc = match state.into_account(addr.clone()) { + Ok(x) => x, + Err(e) => { + error!("Game account parsing error: {}", e.to_string()); + unsub().await; + yield None; + break; + } + }; + yield Some(acc); + } + yield None; + })) + } + async fn get_game_account(&self, addr: &str, mode: QueryMode) -> Result> { let game_account_pubkey = Self::parse_pubkey(addr)?; let game_state = self .internal_get_game_state(&game_account_pubkey, mode) .await?; - Ok(Some(game_state.into_account(addr))) + Ok(Some(game_state.into_account(addr)?)) } async fn get_game_bundle(&self, addr: &str) -> Result> { let mint_pubkey = Self::parse_pubkey(addr)?; + let metaplex_program_id = Pubkey::from_str(METAPLEX_PROGRAM_ID).unwrap(); - let (metadata_account_pubkey, _) = - metaplex_program::pda::find_metadata_account(&mint_pubkey); + let (metadata_account_pubkey, _) = Pubkey::find_program_address( + &[ + b"metadata", + metaplex_program_id.as_ref(), + mint_pubkey.as_ref(), + ], + &metaplex_program_id, + ); let metadata_account_data = self .client .get_account_data(&metadata_account_pubkey) .map_err(|e| TransportError::NetworkError(e.to_string()))?; - let metadata_account_state = Metadata::deserialize(&mut metadata_account_data.as_slice()) - .map_err(|_| TransportError::MetadataDeserializeError)?; + let metadata_account_state = + match metadata::Metadata::deserialize(&mut metadata_account_data.as_slice()) { + Ok(x) => x, + Err(e) => { + error!("Failed to deserialize metadata account: {:?}", e); + return Err(TransportError::MetadataDeserializeError)?; + } + }; let metadata_data = metadata_account_state.data; let uri = metadata_data.uri.trim_end_matches('\0').to_string(); @@ -930,7 +1055,7 @@ impl TransportT for SolanaTransport { // Add amount information by querying stake accounts for (i, stake_addr) in stake_addrs.iter().enumerate() { tracing::info!("Check stake account: {}", stake_addr); - let mut slot = recipient_account + let slot = recipient_account .slots .get_mut(i) .ok_or(Error::TransportError( @@ -1055,8 +1180,9 @@ impl SolanaTransport { CommitmentConfig::finalized() }; let debug = skip_preflight; - let client = RpcClient::new_with_commitment(rpc, commitment); + let client = RpcClient::new_with_commitment(rpc.clone(), commitment); Ok(Self { + rpc, client, keypair, program_id, @@ -1069,6 +1195,22 @@ impl SolanaTransport { .map_err(|_| TransportError::InvalidConfig(format!("Can't parse public key: {}", addr))) } + fn get_recent_prioritization_fees(&self, pubkeys: &[Pubkey]) -> TransportResult { + let fees = self + .client + .get_recent_prioritization_fees(pubkeys) + .map_err(|e| TransportError::FeeCalculationError(e.to_string()))?; + let mut fee = 0; + for f in fees { + if f.prioritization_fee > fee { + fee = f.prioritization_fee; + } + } + println!("Estimate fee: {}", fee); + // XXX: We add a fixed amount to recommended fee + Ok(fee + 50) + } + fn get_min_lamports(&self, account_len: usize) -> TransportResult { self.client .get_minimum_balance_for_rent_exemption(account_len) diff --git a/transport/src/solana/metadata.rs b/transport/src/solana/metadata.rs new file mode 100644 index 00000000..60734f03 --- /dev/null +++ b/transport/src/solana/metadata.rs @@ -0,0 +1,94 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_sdk::pubkey::Pubkey; + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum Key { + Uninitialized, + EditionV1, + MasterEditionV1, + ReservationListV1, + MetadataV1, + ReservationListV2, + MasterEditionV2, + EditionMarker, + UseAuthorityRecord, + CollectionAuthorityRecord, + TokenOwnedEscrow, + TokenRecord, + MetadataDelegate, + EditionMarkerV2, + HolderDelegate, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Creator { + pub address: Pubkey, + pub verified: bool, + pub share: u8, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Data { + pub name: String, + pub symbol: String, + pub uri: String, + pub seller_fee_basis_points: u16, + pub creators: Option>, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum TokenStandard { + NonFungible, + FungibleAsset, + Fungible, + NonFungibleEdition, + ProgrammableNonFungible, + ProgrammableNonFunibleEdition, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Collection { + pub verified: bool, + pub key: Pubkey, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum UseMethod { + Burn, + Multiple, + Single, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Uses { + pub use_method: UseMethod, + pub remaining: u64, + pub total: u64, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum CollectionDetails { + V1 { size: u64 }, + V2 { padding: [u8; 8] }, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub enum ProgrammableConfig { + V1 { rule_set: Option }, +} + +#[derive(BorshDeserialize, BorshSerialize)] +pub struct Metadata { + pub key: Key, + pub update_authority: Pubkey, + pub mint: Pubkey, + pub data: Data, + pub primary_sale_happened: bool, + pub is_mutable: bool, + pub edition_nonce: Option, + pub token_standard: Option, + pub collection: Option, + pub uses: Option, + pub collection_details: Option, + pub programmable_config: Option, +} diff --git a/transport/src/solana/types/state/game.rs b/transport/src/solana/types/state/game.rs index b0b79c20..7b715084 100644 --- a/transport/src/solana/types/state/game.rs +++ b/transport/src/solana/types/state/game.rs @@ -1,5 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use race_core::types::{GameAccount, VoteType, EntryType}; +use race_api::error::Error; +use race_core::{ + checkpoint::CheckpointOnChain, + types::{EntryType, GameAccount, VoteType}, +}; use solana_sdk::pubkey::Pubkey; #[cfg_attr(test, derive(PartialEq, Eq))] @@ -95,12 +99,10 @@ pub struct GameState { pub recipient_addr: Pubkey, // the checkpoint state pub checkpoint: Vec, - // the value of access version when checkpoint is set - pub checkpoint_access_version: u64, } impl GameState { - pub fn into_account>(self, addr: S) -> GameAccount { + pub fn into_account>(self, addr: S) -> Result { let GameState { title, bundle_addr, @@ -117,14 +119,21 @@ impl GameState { entry_type, recipient_addr, checkpoint, - checkpoint_access_version, .. } = self; let players = players.into_iter().map(Into::into).collect(); let servers = servers.into_iter().map(Into::into).collect(); + let checkpoint_onchain = if checkpoint.is_empty() { + Some( + CheckpointOnChain::try_from_slice(&checkpoint) + .map_err(|_| Error::MalformedCheckpoint)?, + ) + } else { + None + }; - GameAccount { + Ok(GameAccount { addr: addr.into(), title, settle_version, @@ -143,8 +152,7 @@ impl GameState { unlock_time: None, recipient_addr: recipient_addr.to_string(), entry_type, - checkpoint, - checkpoint_access_version, - } + checkpoint_on_chain: checkpoint_onchain, + }) } }