diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml deleted file mode 100644 index 64e1443..0000000 --- a/.github/workflows/rust-test.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Kalatori Tests - -on: - pull_request: - push: - branches: - - main - - stable - -jobs: - check: - name: Cargo and TypeScript Tests - runs-on: ubuntu-latest - steps: - - - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ github.token }} - - - name: Checkout sources - uses: actions/checkout@v4.2.0 - with: - fetch-depth: 50 - submodules: recursive - - - name: Initialize Git Submodules - run: git submodule update --init --recursive - - - name: Verify directory structure - run: ls -R - - - name: Install Rust stable toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - toolchain: stable - override: true - - - name: Install cargo-nextest - uses: baptiste0928/cargo-install@v3 - with: - crate: cargo-nextest - version: 0.9 - - - name: Rust Cache - uses: Swatinem/rust-cache@v2.7.5 - - - name: Run Rust app in background with environment variables - run: | - export KALATORI_HOST="127.0.0.1:16726" - export KALATORI_SEED="bottom drive obey lake curtain smoke basket hold race lonely fit walk" - export KALATORI_RECIPIENT="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" - export KALATORI_REMARK="test" - cargo build - nohup cargo r & - - - name: Wait for Rust app to start - run: sleep 120 - # Wait for the Rust app to start and then wait for the app to connect to RPC - - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: '20' - - - name: Install Yarn package manager - run: npm install --global yarn - - - name: Install dependencies - working-directory: ./tests/kalatori-api-test-suite - run: yarn install --network-timeout 100000 - - - name: Run tests - working-directory: ./tests/kalatori-api-test-suite - env: - DAEMON_HOST: 'http://127.0.0.1:16726' - run: yarn test - -# - name: Run Rust tests -# run: cargo nextest run diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee5b62..be68587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file. +## [0.2.6] - 2024-11-01 + +### 🚀 Features + +- Force withdrawal call implementation +- Docker container for the app +- Containerized test environment + +### 🐛 Bug Fixes + +- Fixed the storage fetching. +- Removed redundant name checks & thereby fixed the connection to Asset Hub chains. + +## [0.2.5] - 2024-10-29 + +### 🚀 Features + +- Callback in case callback url provided + +### 🐛 Bug Fixes + +- fix error handling as a result of dep uupgrade +- fix order withdraw transaction +- mark order withdrawn on successful withdraw + ## [0.2.4] - 2024-10-21 ### ⚡ Performance diff --git a/Cargo.lock b/Cargo.lock index 07697e7..1fd6d5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,9 +51,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "611cc2ae7d2e242c457e4be7f97036b8ad9ca152b499f53faf99b1ed8fc2553f" [[package]] name = "anstream" @@ -156,6 +156,12 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -319,6 +325,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byte-slice-cast" version = "1.2.2" @@ -345,9 +357,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.36" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ "shlex", ] @@ -755,6 +767,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -798,6 +819,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf172ba7bfe5412e03c4dfd7d8e4b5f1e6cd0b7087fd61fa274b73f87ad94854" +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "fdeflate" version = "0.3.6" @@ -860,6 +887,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1038,6 +1080,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1136,6 +1197,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1144,6 +1206,40 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1153,13 +1249,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1323,6 +1422,15 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.2" @@ -1368,6 +1476,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "is_debug" version = "1.0.1" @@ -1406,6 +1520,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "jsonrpsee" version = "0.24.7" @@ -1499,7 +1622,7 @@ dependencies = [ [[package]] name = "kalatori" -version = "0.2.4" +version = "0.2.7" dependencies = [ "ahash", "async-lock", @@ -1514,6 +1637,7 @@ dependencies = [ "names", "parity-scale-codec", "primitive-types", + "reqwest", "scale-info", "serde", "serde_json", @@ -1523,14 +1647,13 @@ dependencies = [ "substrate-constructor", "substrate-crypto-light", "substrate_parser", - "thiserror 2.0.0", + "thiserror 2.0.1", "time", "tokio", "tokio-util", "toml_edit", "tracing", "tracing-subscriber", - "ureq", "zeroize", ] @@ -1563,9 +1686,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.161" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" [[package]] name = "libm" @@ -1681,6 +1804,23 @@ dependencies = [ "rand", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1756,12 +1896,50 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1905,6 +2083,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "plot_icon" version = "0.3.0" @@ -1953,6 +2137,7 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec", + "impl-serde", "uint", ] @@ -2097,6 +2282,49 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -2354,9 +2582,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" dependencies = [ "core-foundation-sys", "libc", @@ -2651,7 +2879,7 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "substrate-constructor" version = "0.1.0" -source = "git+https://github.com/Alzymologist/substrate-constructor#540559207e640bfa158358cdbf736eb488c57100" +source = "git+https://github.com/Alzymologist/substrate-constructor#b18997ccd2dccc5b53b39cfaed8b8591de1ee904" dependencies = [ "bitvec", "external-memory-tools", @@ -2691,7 +2919,7 @@ dependencies = [ [[package]] name = "substrate_parser" version = "0.6.1" -source = "git+https://github.com/Alzymologist/substrate-parser#09f98462f5179bf3c5b6c323c53f7438caf03f06" +source = "git+https://github.com/Alzymologist/substrate-parser#12dd47d784339020b3a0c8ebcb09cc7399b7861c" dependencies = [ "bitvec", "external-memory-tools", @@ -2746,6 +2974,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2758,12 +2989,46 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2794,11 +3059,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" +checksum = "07c1e40dd48a282ae8edc36c732cbc219144b87fb6a4c7316d611c6b1f06ec0c" dependencies = [ - "thiserror-impl 2.0.0", + "thiserror-impl 2.0.1", ] [[package]] @@ -2814,9 +3079,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" +checksum = "874aa7e446f1da8d9c3a5c95b1c5eb41d800045252121dc7f8e0ba370cee55f5" dependencies = [ "proc-macro2", "quote", @@ -2878,11 +3143,12 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", + "bytes", "libc", "mio", "pin-project-lite", @@ -2903,6 +3169,16 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -3074,6 +3350,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "twox-hash" version = "1.6.3" @@ -3128,20 +3410,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "ureq" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" -dependencies = [ - "base64", - "log", - "once_cell", - "serde", - "serde_json", - "url", -] - [[package]] name = "url" version = "2.5.3" @@ -3177,6 +3445,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3193,12 +3467,98 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.6" @@ -3239,6 +3599,36 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 3b18b58..c6271c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "kalatori" authors = ["Alzymologist Oy "] -version = "0.2.4" +version = "0.2.7" edition = "2021" description = "A gateway daemon for Kalatori." license = "GPL-3.0-or-later" @@ -20,7 +20,6 @@ axum = { version = "0.7", default-features = false, features = [ "matched-path", ] } tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] } -ureq = { version = "2", default-features = false, features = ["json"] } names = { version = "0.14", default-features = false } tokio-util = { version = "0.7", features = ["rt"] } tokio = { version = "1", features = ["rt-multi-thread", "signal", "time"] } @@ -28,7 +27,7 @@ serde = { version = "1", features = ["derive", "rc"] } tracing = "0.1" scale-info = "2" axum-macros = "0.4" -primitive-types = { version = "0.12", features = ["codec"] } +primitive-types = { version = "0.12", features = ["codec", "serde"] } jsonrpsee = { version = "0.24", features = ["ws-client"] } thiserror = "2" frame-metadata = "16" @@ -60,6 +59,7 @@ ahash = "0.8" indoc = "2" async-lock = "3" time = "0.3" +reqwest = "0.12" substrate_parser = { git = "https://github.com/Alzymologist/substrate-parser" } substrate-constructor = { git = "https://github.com/Alzymologist/substrate-constructor" } @@ -97,7 +97,7 @@ cargo_common_metadata = "warn" arithmetic_side_effects = "warn" # Having multiple module layout styles in a project can be confusing. Moreover, `mod.rs` files don't # really harmonize with Git as they can be renamed back to self-named modules and Git fundamentally -# prone to poorly handling the file renaming that can result in their history erasure. +# prone to poorly handle the file renaming that can result in their history erasure. mod_module_files = "warn" # TODO: https://github.com/rust-lang/cargo/issues/12918 pedantic = { level = "warn", priority = -1 } diff --git a/Dockerfile b/Dockerfile index 9403618..f7dbaf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,29 @@ -FROM ubuntu:latest -RUN mkdir /app -COPY target/release/kalatori /app/kalatori +FROM rust:1.82 as builder -ENV KALATORI_HOST="0.0.0.0:16726" -ENV KALATORI_RPC="wss://rpc.ibp.network/polkadot" +WORKDIR /usr/src/kalatori + +COPY Cargo.toml Cargo.lock ./ + +RUN mkdir -p src && echo "fn main() {}" > src/main.rs + +RUN cargo build --release + +RUN rm -rf src +COPY . . + +RUN cargo build --release + +FROM ubuntu:latest WORKDIR /app + +COPY --from=builder /usr/src/kalatori/target/release/kalatori /app/kalatori + EXPOSE 16726 -CMD ["./kalatori"] +ENV KALATORI_HOST="0.0.0.0:16726" +ENV KALATORI_SEED="bottom drive obey lake curtain smoke basket hold race lonely fit walk" +ENV KALATORI_RECIPIENT="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" +ENV KALATORI_REMARK="test" + +CMD ["/app/kalatori"] diff --git a/configs/chopsticks.toml b/configs/chopsticks.toml index d4f540c..8958d32 100644 --- a/configs/chopsticks.toml +++ b/configs/chopsticks.toml @@ -8,21 +8,18 @@ native-token = "DOT" decimals = 10 endpoints = [ "ws://localhost:8000", - "ws://localhost:8500", ] -[[chain]] -name = "statemint" -decimals = 6 -endpoints = [ - "ws://localhost:9000", - "ws://localhost:9500", -] +# [[chain]] +# name = "statemint" +# endpoints = [ +# "ws://localhost:9000", +# ] -[[chain.asset]] -name = "USDC" -id = 1337 +# [[chain.asset]] +# name = "USDC" +# id = 1337 -[[chain.asset]] -name = "USDt" -id = 1984 +# [[chain.asset]] +# name = "USDt" +# id = 1984 diff --git a/src/arguments.rs b/src/arguments.rs index 738dfd9..57278dc 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -147,7 +147,7 @@ impl SeedEnvVars { #[serde(rename_all = "kebab-case")] pub struct Config { pub account_lifetime: Timestamp, - #[serde(default = "default_host")] + #[serde(default = "get_host")] pub host: SocketAddr, pub database: Option, pub debug: Option, @@ -165,6 +165,7 @@ impl Config { } } -fn default_host() -> SocketAddr { - SOCKET_DEFAULT +fn get_host() -> SocketAddr { + let host = env::var("KALATORI_HOST").unwrap_or_else(|_| "127.0.0.1:16726".to_string()); + host.parse().unwrap_or(SOCKET_DEFAULT) } diff --git a/src/callback.rs b/src/callback.rs deleted file mode 100644 index 4e86e6c..0000000 --- a/src/callback.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::definitions::api_v2::OrderStatus; -use tokio::task; - -pub const MODULE: &str = module_path!(); - -// TODO: This will be used once we setup callback functionality -#[allow(dead_code)] -pub async fn callback(path: String, order_status: OrderStatus) { - let req = ureq::post(&path); - - task::spawn_blocking(move || { - let _d = req.send_json(order_status).unwrap(); - }) - .await - .unwrap(); -} diff --git a/src/chain/definitions.rs b/src/chain/definitions.rs index a09b845..e38724d 100644 --- a/src/chain/definitions.rs +++ b/src/chain/definitions.rs @@ -19,7 +19,7 @@ use tokio::sync::oneshot; /// Abstraction to distinguish block hash from many other H256 things #[derive(Debug, Clone)] -pub struct BlockHash(pub primitive_types::H256); +pub struct BlockHash(pub H256); impl BlockHash { /// Convert block hash to RPC-friendly format @@ -45,45 +45,7 @@ pub struct EventFilter<'a> { pub pallet: &'a str, pub optional_event_variant: Option<&'a str>, } -/* -#[derive(Debug)] -struct ChainProperties { - specs: ShortSpecs, - metadata: RuntimeMetadataV15, - existential_deposit: Option, - assets_pallet: Option, - block_hash_count: BlockNumber, - account_lifetime: BlockNumber, - depth: Option, -} - -#[derive(Debug)] -struct AssetsPallet { - multi_location: Option, - assets: HashMap, -} - -#[derive(Debug)] -struct AssetProperties { - min_balance: Balance, - decimals: Decimals, -} - -#[derive(Debug)] -pub struct Currency { - chain: String, - asset: Option, -} - -#[derive(Debug)] -pub struct ConnectedChain { - rpc: String, - client: WsClient, - genesis: BlockHash, - properties: ChainProperties, -} -*/ pub enum ChainRequest { WatchAccount(WatchAccount), Reap(WatchAccount), @@ -112,7 +74,7 @@ impl WatchAccount { Ok(WatchAccount { id, address: AccountId32::from_base58_string(&order.payment_account) - .map_err(ChainError::InvoiceAccount)? + .map_err(|e| ChainError::InvoiceAccount(e.to_string()))? .0, currency: order.currency, amount: order.amount, @@ -127,6 +89,7 @@ pub enum ChainTrackerRequest { WatchAccount(WatchAccount), NewBlock(BlockNumber), Reap(WatchAccount), + ForceReap(WatchAccount), Shutdown(oneshot::Sender<()>), } diff --git a/src/chain/payout.rs b/src/chain/payout.rs index 5d8a3cf..5291e50 100644 --- a/src/chain/payout.rs +++ b/src/chain/payout.rs @@ -18,7 +18,7 @@ use crate::{ }, database::{TransactionInfoDb, TransactionInfoDbInner, TxKind}, definitions::{ - api_v2::{Amount, TokenKind, TransactionInfo, TxStatus}, + api_v2::{Amount, TokenKind, TxStatus}, Balance, }, error::ChainError, @@ -27,13 +27,13 @@ use crate::{ }; use frame_metadata::v15::RuntimeMetadataV15; use jsonrpsee::ws_client::WsClientBuilder; -use substrate_constructor::fill_prepare::{SpecialTypeToFill, TypeContentToFill}; +use substrate_constructor::fill_prepare::TypeContentToFill; use substrate_crypto_light::common::AsBase58; -use tokio::sync::mpsc::Sender as MpscSender; /// Single function that should completely handle payout attmept. Just do not call anything else. /// /// TODO: make this an additional runner independent from chain monitors +#[expect(clippy::too_many_lines)] pub async fn payout( rpc: String, order: Invoice, @@ -49,7 +49,8 @@ pub async fn payout( let block_number = current_block_number(&client, &chain.metadata, &block).await?; let balance = order.balance(&client, &chain, &block).await?; // TODO same let loss_tolerance = 10000; // TODO: replace with multiple of existential - let manual_intervention_amount = 1000000000000; + // TODO: add upper limit for transactions that would require manual intervention + // just because it was found to be needed with non-crypto trade, who knows why? let currency = chain .assets .get(&order.currency.currency) @@ -57,8 +58,11 @@ pub async fn payout( let order_amount = Balance::parse(order.amount, order.currency.decimals); // Payout operation logic - let transactions = match balance.0 - order_amount.0 { - a if (0..=loss_tolerance).contains(&a) => match currency.kind { + let transactions = if balance.0.abs_diff(order_amount.0) <= loss_tolerance + // modulus(balance-order.amount) <= loss_tolerance + { + tracing::info!("Regular withdrawal"); + match currency.kind { TokenKind::Native => { let balance_transfer_constructor = BalanceTransferConstructor { amount: order_amount.0, @@ -81,14 +85,35 @@ pub async fn payout( &asset_transfer_constructor, )?] } - }, - a if (loss_tolerance..=manual_intervention_amount).contains(&a) => { - tracing::warn!("Overpayments not handled yet"); - return Ok(()); //TODO } - _ => { - tracing::error!("Balance is out of range: {balance:?}"); - return Ok(()); //TODO + } else { + tracing::info!("Overpayment or forced"); + // We will transfer all the available balance + // TODO smarter handling and returns probably + + match currency.kind { + TokenKind::Native => { + let balance_transfer_constructor = BalanceTransferConstructor { + amount: balance.0, + to_account: &order.recipient, + is_clearing: true, + }; + vec![construct_single_balance_transfer_call( + &chain.metadata, + &balance_transfer_constructor, + )?] + } + TokenKind::Asset => { + let asset_transfer_constructor = AssetTransferConstructor { + asset_id: currency.asset_id.ok_or(ChainError::AssetId)?, + amount: balance.0, + to_account: &order.recipient, + }; + vec![construct_single_asset_transfer_call( + &chain.metadata, + &asset_transfer_constructor, + )?] + } } }; @@ -110,8 +135,13 @@ pub async fn payout( let signature = signer.sign(order.id.clone(), sign_this).await?; - batch_transaction.signature.content = - TypeContentToFill::SpecialType(SpecialTypeToFill::SignatureSr25519(Some(signature))); + if let TypeContentToFill::Variant(ref mut multisig) = batch_transaction.signature.content { + if let TypeContentToFill::ArrayU8(ref mut sr25519) = + multisig.selected.fields_to_fill[0].type_to_fill.content + { + sr25519.content = signature.0.to_vec(); + } + } let extrinsic = batch_transaction .send_this_signed::<(), RuntimeMetadataV15>(&chain.metadata)? @@ -133,13 +163,14 @@ pub async fn payout( kind: TxKind::Withdrawal, }, }, - order.id, + order.id.clone(), ) .await .map_err(|_| ChainError::TransactionNotSaved)?; send_stuff(&client, &encoded_extrinsic).await?; + state.order_withdrawn(order.id).await; // TODO obvious } Ok(()) diff --git a/src/chain/rpc.rs b/src/chain/rpc.rs index 05f5c9b..dbb055f 100644 --- a/src/chain/rpc.rs +++ b/src/chain/rpc.rs @@ -24,8 +24,9 @@ use hashing::twox_128; use jsonrpsee::core::client::{ClientT, Subscription, SubscriptionClientT}; use jsonrpsee::rpc_params; use jsonrpsee::ws_client::WsClient; +use primitive_types::U256; use scale_info::{form::PortableForm, PortableRegistry, TypeDef, TypeDefPrimitive}; -use serde::Deserialize; +use serde::{de, Deserialize, Deserializer}; use serde_json::{Number, Value}; use std::{collections::HashMap, fmt::Debug}; use substrate_crypto_light::common::AccountId32; @@ -104,38 +105,40 @@ pub async fn get_keys_from_storage( const_hex::encode(twox_128(storage_name.as_bytes())) ); - let count = 10; // TODO make full scan just in case - let mut start_key: Option = None; // Start from the beginning + let count = 10; + // Because RPC API accepts parameters as a sequence and the last 2 parameters are + // `start_key: Option` and `hash: Option`, API *always* takes `hash` as + // `storage_key` if the latter is `None` and believes that `hash` is `None` because although + // `StorageKey` and `Hash` are different types, any `Hash` perfectly deserializes as + // `StorageKey`. Therefore, `start_key` must always be present to correctly use the + // `state_getKeysPaged` call with the `hash` parameter. + let mut start_key: String = "0x".into(); // Start from the beginning let params_template = vec![ serde_json::to_value(storage_key_prefix.clone()).unwrap(), serde_json::to_value(count).unwrap(), ]; - for i in 0..MAX_KEY_PAGES { + for _ in 0..MAX_KEY_PAGES { let mut params = params_template.clone(); - if let Some(ref start_key) = start_key { - params.push(serde_json::to_value(start_key.clone()).unwrap()); - } + params.push(serde_json::to_value(start_key.clone()).unwrap()); params.push(serde_json::to_value(block.to_string()).unwrap()); if let Ok(keys) = client.request("state_getKeysPaged", params).await { - if let Value::Array(ref keys_inside) = keys { - if keys_inside.len() == 0 { + if let Value::Array(keys_inside) = &keys { + if keys_inside.is_empty() { return Ok(keys_vec); } - if let Some(last) = keys_inside.last() { - if let Value::String(key_string) = last { - start_key = Some(key_string.clone()) - } else { - return Ok(keys_vec); - } + + if let Some(Value::String(key_string)) = keys_inside.last() { + start_key.clone_from(key_string); } else { return Ok(keys_vec); } } else { return Ok(keys_vec); - }; + } + keys_vec.push(keys); } else { return Ok(keys_vec); @@ -260,11 +263,19 @@ pub async fn next_block( pub struct BlockHead { //digest: Value, //extrinsics_root: String, + #[serde(deserialize_with = "deserialize_block_number")] pub number: BlockNumber, //parent_hash: String, //state_root: String, } +fn deserialize_block_number<'d, D: Deserializer<'d>>(d: D) -> Result { + let n = U256::deserialize(d)?; + + n.try_into() + .map_err(|_| de::Error::custom("Try from failed")) +} + #[derive(Deserialize)] pub struct BlockDetails { block: Block, @@ -272,6 +283,11 @@ pub struct BlockDetails { #[derive(Deserialize)] pub struct Block { + pub block: BlockInner, +} + +#[derive(Deserialize)] +pub struct BlockInner { pub extrinsics: Vec, } @@ -287,18 +303,6 @@ pub async fn assets_set_at_block( let mut assets_set = HashMap::new(); let chain_name = >::spec_name_version(metadata_v15)?.spec_name; - assets_set.insert( - specs.unit, - CurrencyProperties { - chain_name: chain_name.clone(), - kind: TokenKind::Native, - decimals: specs.decimals, - rpc_url: rpc_url.to_owned(), - asset_id: None, - ss58: specs.base58prefix, - }, - ); - let mut assets_asset_storage_metadata = None; let mut assets_metadata_storage_metadata = None; @@ -658,6 +662,7 @@ pub async fn transfer_events( &metadata_v15.types, ) .await?; + tracing::error!("{events:?}"); match_extrinsics_with_events_at_block(events, client, block, metadata_v15).await } @@ -672,6 +677,7 @@ async fn match_extrinsics_with_events_at_block( .request("chain_getBlock", rpc_params!(block_hash.to_string())) .await?; let extrinsics = block + .block .extrinsics .into_iter() .map(|encoded| unhex(&encoded, NotHexError::Extrinsic)) @@ -740,101 +746,81 @@ async fn events_at_block( events_entry_metadata: &StorageEntryMetadata, types: &PortableRegistry, ) -> Result, Event)>, ChainError> { - let keys_from_storage_vec = get_keys_from_storage(client, "System", "Events", block).await?; + let key = format!( + "0x{}{}", + const_hex::encode(twox_128("System".as_bytes())), + const_hex::encode(twox_128("Events".as_bytes())) + ); let mut out = Vec::new(); - for keys_from_storage in keys_from_storage_vec { - match keys_from_storage { - Value::Array(ref keys_array) => { - for key in keys_array { - if let Value::String(key) = key { - let data_from_storage = get_value_from_storage(client, &key, block).await?; - let key_bytes = unhex(&key, NotHexError::StorageValue)?; - let value_bytes = - if let Value::String(data_from_storage) = data_from_storage { - unhex(&data_from_storage, NotHexError::StorageValue)? - } else { - return Err(ChainError::StorageValueFormat(data_from_storage)); - }; - let storage_data = - decode_as_storage_entry::<&[u8], (), RuntimeMetadataV15>( - &key_bytes.as_ref(), - &value_bytes.as_ref(), - &mut (), - events_entry_metadata, - types, - ) - .expect("RAM stored metadata access"); - if let ParsedData::SequenceRaw(sequence_raw) = storage_data.value.data { - for sequence_element in sequence_raw.data { - let (mut extrinsic_index, mut event_option) = (None, None); - - if let ParsedData::Composite(event_record) = sequence_element { - for event_record_element in event_record { - match event_record_element.field_name.as_deref() { - Some("event") => { - if let ParsedData::Event(Event(event)) = - event_record_element.data.data - { - if let Some(filter) = &optional_filter { - if let Some(event_variant) = - filter.optional_event_variant - { - if event.pallet_name == filter.pallet - && event.variant_name - == event_variant - { - event_option = Some(Event(event)); - } - } else if event.pallet_name == filter.pallet - { - event_option = Some(Event(event)); - } - } else { - event_option = Some(Event(event)); - } - } - } - Some("phase") => { - if let ParsedData::Variant(VariantData { - variant_name, - fields, - .. - }) = event_record_element.data.data - { - if variant_name == "ApplyExtrinsic" { - if let Some(FieldData { - data: - ExtendedData { - data: - ParsedData::PrimitiveU32 { - value, - .. - }, - .. - }, - .. - }) = fields.into_iter().next() - { - extrinsic_index = Some(value); - } - } - } - } - _ => {} + let data_from_storage = get_value_from_storage(client, &key, block).await?; + let key_bytes = unhex(&key, NotHexError::StorageValue)?; + let value_bytes = if let Value::String(data_from_storage) = data_from_storage { + unhex(&data_from_storage, NotHexError::StorageValue)? + } else { + return Err(ChainError::StorageValueFormat(data_from_storage)); + }; + let storage_data = decode_as_storage_entry::<&[u8], (), RuntimeMetadataV15>( + &key_bytes.as_ref(), + &value_bytes.as_ref(), + &mut (), + events_entry_metadata, + types, + ) + .expect("RAM stored metadata access"); + if let ParsedData::SequenceRaw(sequence_raw) = storage_data.value.data { + for sequence_element in sequence_raw.data { + let (mut extrinsic_index, mut event_option) = (None, None); + + if let ParsedData::Composite(event_record) = sequence_element { + for event_record_element in event_record { + match event_record_element.field_name.as_deref() { + Some("event") => { + if let ParsedData::Event(Event(event)) = event_record_element.data.data + { + if let Some(filter) = &optional_filter { + if let Some(event_variant) = filter.optional_event_variant { + if event.pallet_name == filter.pallet + && event.variant_name == event_variant + { + event_option = Some(Event(event)); } + } else if event.pallet_name == filter.pallet { + event_option = Some(Event(event)); } + } else { + event_option = Some(Event(event)); } - - if let Some(event_some) = event_option { - out.push((extrinsic_index, event_some)); + } + } + Some("phase") => { + if let ParsedData::Variant(VariantData { + variant_name, + fields, + .. + }) = event_record_element.data.data + { + if variant_name == "ApplyExtrinsic" { + if let Some(FieldData { + data: + ExtendedData { + data: ParsedData::PrimitiveU32 { value, .. }, + .. + }, + .. + }) = fields.into_iter().next() + { + extrinsic_index = Some(value); + } } } } + _ => {} } } } - _ => { - tracing::warn!("{keys_from_storage}"); + + if let Some(event_some) = event_option { + out.push((extrinsic_index, event_some)); } } } diff --git a/src/chain/tracker.rs b/src/chain/tracker.rs index 8d0523c..fac8ef9 100644 --- a/src/chain/tracker.rs +++ b/src/chain/tracker.rs @@ -1,18 +1,5 @@ //! A tracker that follows individual chain -use frame_metadata::v15::RuntimeMetadataV15; -use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; -use serde_json::Value; -use std::{collections::HashMap, time::SystemTime}; -use substrate_crypto_light::common::AsBase58; -use substrate_parser::{AsMetadata, ShortSpecs}; -use time::{format_description::well_known::Rfc3339, OffsetDateTime}; -use tokio::{ - sync::mpsc, - time::{timeout, Duration}, -}; -use tokio_util::sync::CancellationToken; - use crate::{ chain::{ definitions::{BlockHash, ChainTrackerRequest, Invoice}, @@ -23,20 +10,28 @@ use crate::{ }, utils::parse_transfer_event, }, - database::{FinalizedTxDb, TransactionInfoDb, TransactionInfoDbInner}, + database::{FinalizedTxDb, TransactionInfoDb, TransactionInfoDbInner, TxKind}, definitions::{ - api_v2::{Amount, CurrencyProperties, TxStatus}, - Balance, Chain, + api_v2::{Amount, CurrencyProperties, Health, RpcInfo, TokenKind, TxStatus}, + Chain, }, error::{ChainError, Error}, signer::Signer, state::State, utils::task_tracker::TaskTracker, }; -use crate::{ - database::TxKind, - definitions::api_v2::{Health, RpcInfo}, +use frame_metadata::v15::RuntimeMetadataV15; +use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; +use serde_json::Value; +use std::{collections::HashMap, time::SystemTime}; +use substrate_crypto_light::common::AsBase58; +use substrate_parser::{AsMetadata, ShortSpecs}; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; +use tokio::{ + sync::mpsc, + time::{timeout, Duration}, }; +use tokio_util::sync::CancellationToken; #[allow(clippy::too_many_lines)] pub fn start_chain_watch( @@ -56,7 +51,7 @@ pub fn start_chain_watch( let mut watched_accounts = HashMap::new(); let mut shutdown = false; // TODO: random pick instead - for endpoint in chain.endpoints.iter().cycle() { + for endpoint in chain.endpoints.clone().iter().cycle() { // not restarting chain if shutdown is in progress if shutdown || cancellation_token.is_cancelled() { break; @@ -132,12 +127,15 @@ pub fn start_chain_watch( ) .await { Ok((timestamp, events)) => { + tracing::error!("{timestamp:?} {events:?}"); + tracing::error!("{watched_accounts:?}"); let mut id_remove_list = Vec::new(); let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64; for (id, invoice) in &watched_accounts { for (extrinsic_option, event) in &events { if let Some((tx_kind, another_account, transfer_amount)) = parse_transfer_event(&invoice.address, &event.0.fields) { + tracing::error!("{tx_kind:?} {another_account:?} {transfer_amount:?}"); let Some((position_in_block, extrinsic)) = extrinsic_option else { return Err(Error::from(ChainError::TransferEventNoExtrinsic)); }; @@ -147,7 +145,6 @@ pub fn start_chain_watch( match invoice.check(&client, &watcher, &block).await { Ok(true) => { state.order_paid(id.clone()).await; - id_remove_list.push(id.to_owned()); } Ok(false) => {} Err(e) => { @@ -199,20 +196,18 @@ pub fn start_chain_watch( id.clone()).await?; } } - } - } + } else if invoice.death.0 >= now { + match invoice.check(&client, &watcher, &block).await { + Ok(paid) => { + if paid { + state.order_paid(id.clone()).await; + } - if invoice.death.0 >= now { - match invoice.check(&client, &watcher, &block).await { - Ok(paid) => { - if paid { - state.order_paid(id.clone()).await; + id_remove_list.push(id.to_owned()); + } + Err(e) => { + tracing::warn!("account fetch error: {0:?}", e); } - - id_remove_list.push(id.to_owned()); - } - Err(e) => { - tracing::warn!("account fetch error: {0:?}", e); } } } @@ -237,13 +232,23 @@ pub fn start_chain_watch( let reap_state_handle = state.interface(); let watcher_for_reaper = watcher.clone(); let signer_for_reaper = signer.interface(); - let chain_tx_clone = chain_tx.clone(); task_tracker.clone().spawn(format!("Initiate payout for order {}", id.clone()), async move { payout(rpc, Invoice::from_request(request), reap_state_handle, watcher_for_reaper, signer_for_reaper).await; Ok(format!("Payout attempt for order {id} terminated")) }); } + ChainTrackerRequest::ForceReap(request) => { + let id = request.id.clone(); + let rpc = endpoint.clone(); + let reap_state_handle = state.interface(); + let watcher_for_reaper = watcher.clone(); + let signer_for_reaper = signer.interface(); + task_tracker.clone().spawn(format!("Initiate forced payout for order {}", id.clone()), async move { + payout(rpc, Invoice::from_request(request), reap_state_handle, watcher_for_reaper, signer_for_reaper).await; + Ok(format!("Forced payout attempt for order {id} terminated")) + }); + } ChainTrackerRequest::Shutdown(res) => { shutdown = true; let _ = res.send(()); @@ -273,6 +278,7 @@ pub struct ChainWatcher { } impl ChainWatcher { + #[expect(clippy::too_many_lines)] pub async fn prepare_chain( client: &WsClient, chain: Chain, @@ -282,11 +288,11 @@ impl ChainWatcher { state: State, task_tracker: TaskTracker, ) -> Result { - let genesis_hash = genesis_hash(&client).await?; - let mut blocks = subscribe_blocks(&client).await?; + let genesis_hash = genesis_hash(client).await?; + let mut blocks = subscribe_blocks(client).await?; let block = next_block(client, &mut blocks).await?; let version = runtime_version_identifier(client, &block).await?; - let metadata = metadata(&client, &block).await?; + let metadata = metadata(client, &block).await?; let name = >::spec_name_version(&metadata)?.spec_name; if name != chain.name { return Err(ChainError::WrongNetwork { @@ -294,10 +300,10 @@ impl ChainWatcher { actual: name, rpc: rpc_url.to_string(), }); - }; - let specs = specs(&client, &metadata, &block).await?; + } + let specs = specs(client, &metadata, &block).await?; let mut assets = - assets_set_at_block(&client, &block, &metadata, rpc_url, specs.clone()).await?; + assets_set_at_block(client, &block, &metadata, rpc_url, specs.clone()).await?; // TODO: make this verbosity less annoying tracing::info!( @@ -307,23 +313,40 @@ impl ChainWatcher { &chain.asset ); // Remove unwanted assets - assets.retain(|name, properties| { + assets.retain(|asset_name, properties| { tracing::info!( "chain {} has token {} with properties {:?}", &chain.name, - &name, - &properties + asset_name, + properties ); - if let Some(native_token) = &chain.native_token { - (native_token.name == *name) && (native_token.decimals == specs.decimals) + + if let Some(ref native_token) = chain.native_token { + (native_token.name == *asset_name) && (native_token.decimals == specs.decimals) } else { chain .asset .iter() - .any(|a| (a.name == *name) && (Some(a.id) == properties.asset_id)) + .any(|a| a.name == *asset_name && Some(a.id) == properties.asset_id) } }); + if let Some(ref native_token) = chain.native_token { + if native_token.decimals == specs.decimals { + assets.insert( + native_token.name.clone(), + CurrencyProperties { + chain_name: name.clone(), + kind: TokenKind::Native, + decimals: specs.decimals, + rpc_url: rpc_url.to_owned(), + asset_id: None, + ss58: 0, + }, + ); + } + } + // Deduplication is done on chain manager level; // Check that we have same number of assets as requested (we've checked that we have only // wanted ones and performed deduplication before) @@ -333,7 +356,7 @@ impl ChainWatcher { // // TODO: maybe check if at least one endpoint responds with proper assets and if not, shut // down - if assets.len() != chain.asset.len() + if chain.native_token.is_some() { 1 } else { 0 } { + if assets.len() != chain.asset.len() + usize::from(chain.native_token.is_some()) { return Err(ChainError::AssetsInvalid(chain.name)); } // this MUST assert that assets match exactly before reporting it diff --git a/src/database.rs b/src/database.rs index 0496d23..df95794 100644 --- a/src/database.rs +++ b/src/database.rs @@ -4,22 +4,20 @@ //! commercial offers and contracts, hence causality is a must. Care must be taken that no threads //! are spawned here other than main database server thread that does everything in series. -use crate::definitions::api_v2::{ - Amount, BlockNumber, ExtrinsicIndex, FinalizedTx, ServerInfo, TransactionInfo, TxStatus, -}; use crate::{ definitions::{ api_v2::{ - CurrencyInfo, OrderCreateResponse, OrderInfo, OrderQuery, PaymentStatus, Timestamp, + Amount, BlockNumber, CurrencyInfo, ExtrinsicIndex, FinalizedTx, OrderCreateResponse, + OrderInfo, OrderQuery, PaymentStatus, ServerInfo, Timestamp, TransactionInfo, TxStatus, WithdrawalStatus, }, Version, }, - error::{DbError, Error}, + error::DbError, utils::task_tracker::TaskTracker, }; use codec::{Decode, Encode}; -use names::{Generator, Name}; +use names::Generator; use sled::Tree; use std::time::SystemTime; use substrate_crypto_light::common::AccountId32; @@ -30,12 +28,6 @@ pub const MODULE: &str = module_path!(); const DB_VERSION: Version = 0; // Tables -/* -const ROOT: TableDefinition<'_, &str, &[u8]> = TableDefinition::new("root"); -const KEYS: TableDefinition<'_, PublicSlot, U256Slot> = TableDefinition::new("keys"); -const CHAINS: TableDefinition<'_, ChainHash, BlockNumber> = TableDefinition::new("chains"); -const INVOICES: TableDefinition<'_, InvoiceKey, Invoice> = TableDefinition::new("invoices"); -*/ const ACCOUNTS: &str = "accounts"; //type ACCOUNTS_KEY = (Option, Account); @@ -44,16 +36,8 @@ const ACCOUNTS: &str = "accounts"; const PENDING_TRANSACTIONS: &str = "pending_transactions"; const TRANSACTIONS: &str = "transactions"; -//type TRANSACTIONS_KEY = BlockNumber; -//type TRANSACTIONS_VALUE = (Account, Nonce, Transfer); - const HIT_LIST: &str = "hit_list"; -//type HIT_LIST_KEY = BlockNumber; -//type HIT_LIST_VALUE = (Option, Account); - -// `ROOT` keys - // The database version must be stored in a separate slot to be used by the not implemented yet // database migration logic. const DB_VERSION_KEY: &str = "db_version"; @@ -72,96 +56,6 @@ type PublicSlot = [u8; 32]; type BalanceSlot = u128; type Derivation = [u8; 32]; pub type Account = [u8; 32]; -/* -#[derive(Encode, Decode)] -enum ChainKind { - Id(Vec>), - MultiLocation(Vec>), -} - -#[derive(Encode, Decode)] -struct DaemonInfo { - chains: Vec<(String, ChainProperties)>, - current_key: PublicSlot, - old_keys_death_timestamps: Vec<(PublicSlot, Timestamp)>, -} - -#[derive(Encode, Decode)] -struct ChainProperties { - genesis: BlockHash, - hash: ChainHash, - kind: ChainKind, -} - -#[derive(Encode, Decode)] -struct Transfer(Option>, #[codec(compact)] BalanceSlot); - -#[derive(Encode, Decode, Debug)] -struct Invoice { - derivation: (PublicSlot, Derivation), - paid: bool, - #[codec(compact)] - timestamp: Timestamp, - #[codec(compact)] - price: BalanceSlot, - callback: String, - message: String, - transactions: TransferTxs, -} - -#[derive(Encode, Decode, Debug)] -enum TransferTxs { - Asset { - #[codec(compact)] - id: AssetId, - // transactions: TransferTxsAsset, - }, - Native { - recipient: Account, - encoded: Vec, - exact_amount: Option>, - }, -} - -// #[derive(Encode, Decode, Debug)] -// struct TransferTxsAsset { -// recipient: Account, -// encoded: Vec, -// #[codec(compact)] -// amount: BalanceSlot, -// } - -#[derive(Encode, Decode, Debug)] -struct TransferTx { - recipient: Account, - exact_amount: Option>, -} -*/ -/* -impl Value for Invoice { - type SelfType<'a> = Self; - - type AsBytes<'a> = Vec; - - fn fixed_width() -> Option { - None - } - - fn from_bytes<'a>(mut data: &[u8]) -> Self::SelfType<'_> - where - Self: 'a, - { - Self::decode(&mut data).unwrap() - } - - fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'a>) -> Self::AsBytes<'_> { - value.encode() - } - - fn type_name() -> TypeName { - TypeName::new(stringify!(Invoice)) - } -}*/ pub struct ConfigWoChains { pub recipient: AccountId32, @@ -221,7 +115,7 @@ impl Database { _ => None, } }) - .filter(|(a, b)| b.payment_status == PaymentStatus::Pending) + .filter(|(_, b)| b.payment_status == PaymentStatus::Pending) .collect())); } DbRequest::CreateOrder(request) => { @@ -248,6 +142,9 @@ impl Database { DbRequest::MarkWithdrawn(request) => { let _unused = request.res.send(mark_withdrawn(request.order, &orders)); } + DbRequest::MarkForced(request) => { + let _unused = request.res.send(mark_forced(request.order, &orders)); + } DbRequest::MarkStuck(request) => { let _unused = request.res.send(mark_stuck(request.order, &orders)); } @@ -381,6 +278,14 @@ impl Database { .await; rx.await.map_err(|_| DbError::DbEngineDown)? } + pub async fn mark_forced(&self, order: String) -> Result<(), DbError> { + let (res, rx) = oneshot::channel(); + let _unused = self + .tx + .send(DbRequest::MarkForced(ModifyOrder { order, res })) + .await; + rx.await.map_err(|_| DbError::DbEngineDown)? + } pub async fn mark_stuck(&self, order: String) -> Result<(), DbError> { let (res, rx) = oneshot::channel(); @@ -404,6 +309,7 @@ enum DbRequest { ReadOrder(ReadOrder), MarkPaid(MarkPaid), MarkWithdrawn(ModifyOrder), + MarkForced(ModifyOrder), MarkStuck(ModifyOrder), InitializeServerInfo(oneshot::Sender>), Shutdown(oneshot::Sender<()>), @@ -485,14 +391,14 @@ fn read_order( tx_table: &Tree, pending_tx_table: &Tree, ) -> Result, DbError> { - let Some(order_encoded) = orders.get(key)? else { + let order_key = key.encode(); + let Some(order_encoded) = orders.get(&order_key)? else { return Ok(None); }; - let encoded_key = key.encode(); let mut order = OrderInfo::decode(&mut &order_encoded[..])?; let transactions = tx_table - .scan_prefix(&encoded_key) + .scan_prefix(&order_key) .map(|result| { result.map_err(DbError::from).and_then(|(k, v)| { let (_order_key, block_number, position_in_block) = @@ -510,7 +416,7 @@ fn read_order( .map_err(Into::into) }) }) - .chain(pending_tx_table.scan_prefix(encoded_key).map(|result| { + .chain(pending_tx_table.scan_prefix(order_key).map(|result| { result.map_err(DbError::from).and_then(|(k, v)| { let (_order_key, transaction_bytes) = <(String, String)>::decode(&mut k.as_ref())?; @@ -551,6 +457,7 @@ fn record_transaction( // transactions. if let Some(_encoded_tx_inner) = pending_tx_table.get(&pending_tx_key)? { if let Some((finalized_tx, _finalized_tx_timestamp)) = finalized_info { + tracing::error!("moving pending to fin"); pending_tx_table.remove(pending_tx_key)?; tx_table.insert( @@ -563,10 +470,12 @@ fn record_transaction( tx.encode(), )?; } else { + tracing::error!("updating pending"); pending_tx_table.insert(pending_tx_key, tx.inner.encode())?; } // Save the given finalized transaction. } else if let Some((finalized_tx, _finalized_tx_timestamp)) = finalized_info { + tracing::error!("save fin"); tx_table.insert( ( order, @@ -579,6 +488,7 @@ fn record_transaction( // Save the pending transaction. } else { + tracing::error!("adding pending"); pending_tx_table.insert(pending_tx_key, tx.inner.encode())?; } @@ -600,9 +510,9 @@ fn mark_paid(order: String, orders: &Tree) -> Result { Err(DbError::OrderNotFound(order)) } } - fn mark_withdrawn(order: String, orders: &Tree) -> Result<(), DbError> { - if let Some(order_info) = orders.get(order.clone())? { + let order_key = order.encode(); + if let Some(order_info) = orders.get(order_key)? { let mut order_info = OrderInfo::decode(&mut &order_info[..])?; if order_info.payment_status == PaymentStatus::Paid { if order_info.withdrawal_status == WithdrawalStatus::Waiting { @@ -620,6 +530,27 @@ fn mark_withdrawn(order: String, orders: &Tree) -> Result<(), DbError> { } } +fn mark_forced(order: String, orders: &Tree) -> Result<(), DbError> { + let order_key = order.encode(); + if let Some(order_info) = orders.get(order_key)? { + let mut order_info = OrderInfo::decode(&mut &order_info[..])?; + if order_info.payment_status == PaymentStatus::Pending + || order_info.payment_status == PaymentStatus::Paid + { + if order_info.withdrawal_status == WithdrawalStatus::Waiting { + order_info.withdrawal_status = WithdrawalStatus::Forced; + orders.insert(order.encode(), order_info.encode())?; + Ok(()) + } else { + Err(DbError::WithdrawalWasAttempted(order)) + } + } else { + Err(DbError::NotPaid(order)) + } + } else { + Err(DbError::OrderNotFound(order)) + } +} fn mark_stuck(order: String, orders: &Tree) -> Result<(), DbError> { if let Some(order_info) = orders.get(order.clone())? { let mut order_info = OrderInfo::decode(&mut &order_info[..])?; @@ -657,7 +588,7 @@ pub struct TransactionInfoDb { pub inner: TransactionInfoDbInner, } -#[derive(Encode, Decode)] +#[derive(Encode, Decode, Debug)] pub enum TxKind { Payment, Withdrawal, @@ -693,282 +624,3 @@ impl From for TransactionInfo { } } } - -//impl StateInterface { -/* - Ok(( - OrderStatus { - order, - payment_status: if invoice.paid { - PaymentStatus::Paid - } else { - PaymentStatus::Pending - }, - message: String::new(), - recipient: state.0.recipient.to_ss58check(), - server_info: state.server_info(), - order_info: OrderInfo { - withdrawal_status: WithdrawalStatus::Waiting, - amount: invoice.amount.format(6), - currency: CurrencyInfo { - currency: "USDC".into(), - chain_name: "assethub-polkadot".into(), - kind: TokenKind::Asset, - decimals: 6, - rpc_url: state.rpc.clone(), - asset_id: Some(1337), - }, - callback: invoice.callback.clone(), - transactions: vec![], - payment_account: invoice.paym_acc.to_ss58check(), - }, - }, - OrderSuccess::Found, - )) -} else { - Ok(( - OrderStatus { - order, - payment_status: PaymentStatus::Unknown, -message: String::new(), - recipient: state.0.recipient.to_ss58check(), - server_info: state.server_info(), - order_info: OrderInfo { - withdrawal_status: WithdrawalStatus::Waiting, - amount: 0f64, - currency: CurrencyInfo { - currency: "USDC".into(), - chain_name: "assethub-polkadot".into(), - kind: TokenKind::Asset, - decimals: 6, - rpc_url: state.rpc.clone(), - asset_id: Some(1337), - }, - callback: String::new(), - transactions: vec![], - payment_account: String::new(), - }, - }, - OrderSuccess::Found, - )) -}*/ - -/* - * -let pay_acc: AccountId = state - .0 - .pair - .derive(vec![DeriveJunction::hard(order.clone())].into_iter(), None) - .unwrap() - .0 - .public() - .into(); - - * */ - -/*( - OrderStatus { - order, - payment_status: PaymentStatus::Pending, - message: String::new(), - recipient: state.0.recipient.to_ss58check(), - server_info: state.server_info(), - order_info: OrderInfo { - withdrawal_status: WithdrawalStatus::Waiting, - amount, - currency: CurrencyInfo { - currency: "USDC".into(), - chain_name: "assethub-polkadot".into(), - kind: TokenKind::Asset, - decimals: 6, - rpc_url: state.rpc.clone(), - asset_id: Some(1337), - }, - callback, - transactions: vec![], - payment_account: pay_acc.to_ss58check(), - }, - }, - OrderSuccess::Created, -))*/ - -/* - ServerStatus { - description: state.server_info(), - supported_currencies: state.currencies.clone(), - } -*/ -/* -#[derive(Deserialize, Debug)] -pub struct Invoicee { - pub callback: String, - pub amount: Balance, - pub paid: bool, - pub paym_acc: Account, -} -*/ -/* - -*/ -/* - pub fn server_info(&self) -> ServerInfo { - ServerInfo { - version: env!("CARGO_PKG_VERSION"), - instance_id: String::new(), - debug: self.debug, - kalatori_remark: self.remark.clone(), - } - } -*/ -/* - pub fn currency_properties(&self, currency_name: &str) -> Result<&CurrencyProperties, ErrorDb> { - self.currencies - .get(currency_name) - .ok_or(ErrorDb::CurrencyKeyNotFound) - } - - pub fn currency_info(&self, currency_name: &str) -> Result { - let currency = self.currency_properties(currency_name)?; - Ok(CurrencyInfo { - currency: currency_name.to_string(), - chain_name: currency.chain_name.clone(), - kind: currency.kind, - decimals: currency.decimals, - rpc_url: currency.rpc_url.clone(), - asset_id: currency.asset_id, - }) - } -*/ -// pub fn rpc(&self) -> &str { -// &self.rpc -// } - -// pub fn destination(&self) -> &Option { -// &self.destination -// } - -// pub fn write(&self) -> Result> { -// self.db -// .begin_write() -// .map(WriteTransaction) -// .context("failed to begin a write transaction for the database") -// } - -// pub fn read(&self) -> Result> { -// self.db -// .begin_read() -// .map(ReadTransaction) -// .context("failed to begin a read transaction for the database") -// } - -// pub async fn properties(&self) -> RwLockReadGuard<'_, ChainProperties> { -// self.properties.read().await -// } - -// pub fn pair(&self) -> &Pair { -// &self.pair -// } - -/* -pub struct ReadTransaction(redb::ReadTransaction); - -impl ReadTransaction { - pub fn invoices(&self) -> Result { - self.0 - .open_table(INVOICES) - .map(ReadInvoices) - .with_context(|| format!("failed to open the `{}` table", INVOICES.name())) - } -} - -pub struct ReadInvoices<'a>(ReadOnlyTable<&'a [u8], Invoice>); - -impl <'a> ReadInvoices<'a> { - pub fn get(&self, account: &Account) -> Result>> { - self.0 - .get(&*account) - .context("failed to get an invoice from the database") - } -*/ -// pub fn try_iter( -// &self, -// ) -> Result, AccessGuard<'_, Invoice>)>>> -// { -// self.0 -// .iter() -// .context("failed to get the invoices iterator") -// .map(|iter| iter.map(|item| item.context("failed to get an invoice from the iterator"))) -// } -// } - -// pub struct WriteTransaction<'db>(redb::WriteTransaction<'db>); - -// impl<'db> WriteTransaction<'db> { -// pub fn root(&self) -> Result> { -// self.0 -// .open_table(ROOT) -// .map(Root) -// .with_context(|| format!("failed to open the `{}` table", ROOT.name())) -// } - -// pub fn invoices(&self) -> Result> { -// self.0 -// .open_table(INVOICES) -// .map(WriteInvoices) -// .with_context(|| format!("failed to open the `{}` table", INVOICES.name())) -// } - -// pub fn commit(self) -> Result<()> { -// self.0 -// .commit() -// .context("failed to commit a write transaction in the database") -// } -// } - -// pub struct WriteInvoices<'db, 'tx>(Table<'db, 'tx, &'static [u8; 32], Invoice>); - -// impl WriteInvoices<'_, '_> { -// pub fn save( -// &mut self, -// account: &Account, -// invoice: &Invoice, -// ) -> Result>> { -// self.0 -// .insert(AsRef::<[u8; 32]>::as_ref(account), invoice) -// .context("failed to save an invoice in the database") -// } -// } - -// pub struct Root<'db, 'tx>(Table<'db, 'tx, &'static str, Vec>); - -// impl Root<'_, '_> { -// pub fn save_last_block(&mut self, number: BlockNumber) -> Result<()> { -// self.0 -// .insert(LAST_BLOCK, Compact(number).encode()) -// .context("context")?; - -// Ok(()) -// } -// } - -// fn get_slot(table: &Table<'_, &str, Vec>, key: &str) -> Result>> { -// table -// .get(key) -// .map(|slot_option| slot_option.map(|slot| slot.value().clone())) -// .with_context(|| format!("failed to get the {key:?} slot")) -// } - -// fn decode_slot(mut slot: &[u8], key: &str) -> Result { -// T::decode(&mut slot).with_context(|| format!("failed to decode the {key:?} slot")) -// } - -// fn insert_daemon_info( -// table: &mut Table<'_, '_, &str, Vec>, -// rpc: String, -// key: Public, -// ) -> Result<()> { -// table -// .insert(DAEMON_INFO, DaemonInfo { rpc, key }.encode()) -// .map(|_| ()) -// .context("failed to insert the daemon info") -// } diff --git a/src/definitions.rs b/src/definitions.rs index 71696fd..313cd77 100644 --- a/src/definitions.rs +++ b/src/definitions.rs @@ -105,7 +105,7 @@ pub mod api_v2 { pub currency: String, } - #[derive(Debug)] + #[derive(Debug, Serialize)] pub enum OrderResponse { NewOrder(OrderStatus), FoundOrder(OrderStatus), diff --git a/src/error.rs b/src/error.rs index e3a910d..398f450 100644 --- a/src/error.rs +++ b/src/error.rs @@ -65,8 +65,8 @@ pub enum Error { #[error("failed to complete {0}")] Task(TaskName, #[source] TaskError), - #[error("receiver account couldn't be parsed")] - RecipientAccount(#[from] CryptoError), + #[error("receiver account couldn't be parsed: {0}")] + RecipientAccount(String), #[error("fatal error is occurred")] Fatal, @@ -75,6 +75,30 @@ pub enum Error { DuplicateCurrency(String), } +impl From for Error { + fn from(err: CryptoError) -> Self { + Error::RecipientAccount(err.to_string()) + } +} + +impl From for ChainError { + fn from(err: CryptoError) -> Self { + ChainError::InvoiceAccount(err.to_string()) + } +} + +impl From for SignerError { + fn from(err: CryptoError) -> Self { + SignerError::InvalidDerivation(err.to_string()) + } +} + +impl From for ChainError { + fn from(err: Error) -> Self { + ChainError::Util(UtilError::NotHex(NotHexError::BlockHash)) + } +} + #[derive(Debug, Error)] pub enum SeedEnvError { #[error("one of the `{OLD_SEED}*` variables has an invalid Unicode key")] @@ -216,7 +240,7 @@ pub enum ChainError { Util(#[from] UtilError), #[error("invoice account couldn't be parsed")] - InvoiceAccount(#[from] CryptoError), + InvoiceAccount(String), #[error("chain {0:?} isn't found")] InvalidChain(String), @@ -372,7 +396,7 @@ pub enum ForceWithdrawalError { InvalidParameter(String), #[error("withdrawal was failed: \"{0:?}\"")] - WithdrawalError(Box), + WithdrawalError(String), } #[derive(Debug, thiserror::Error)] @@ -405,7 +429,7 @@ pub enum SignerError { InvalidSeed(#[from] ErrorWordList), #[error("derivation was failed")] - InvalidDerivation(#[from] CryptoError), + InvalidDerivation(String), } #[derive(Debug, Eq, PartialEq, thiserror::Error)] diff --git a/src/handlers/order.rs b/src/handlers/order.rs index 901be99..2608d6e 100644 --- a/src/handlers/order.rs +++ b/src/handlers/order.rs @@ -120,11 +120,9 @@ pub async fn order( pub async fn process_force_withdrawal( state: State, order_id: String, -) -> Result { - state - .force_withdrawal(order_id) - .await - .map_err(|e| ForceWithdrawalError::WithdrawalError(e.into())) +) -> Result { + let response = state.force_withdrawal(order_id).await?; + Ok(response) } pub async fn force_withdrawal( @@ -132,7 +130,10 @@ pub async fn force_withdrawal( Path(order_id): Path, ) -> Response { match process_force_withdrawal(state, order_id).await { - Ok(a) => (StatusCode::CREATED, Json(a)).into_response(), + Ok(OrderResponse::FoundOrder(order_status)) => { + (StatusCode::CREATED, Json(order_status)).into_response() + } + Ok(OrderResponse::NotFound) => (StatusCode::NOT_FOUND, "Order not found").into_response(), Err(ForceWithdrawalError::WithdrawalError(a)) => { (StatusCode::BAD_REQUEST, Json(a)).into_response() } @@ -152,6 +153,11 @@ pub async fn force_withdrawal( }]), ) .into_response(), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Unexpected response type for force withdrawal", + ) + .into_response(), } } diff --git a/src/lib.rs b/src/lib.rs index 26d40d8..f110379 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ // codebase and make changes only to this file. mod arguments; -mod callback; mod chain; mod database; mod definitions; diff --git a/src/main.rs b/src/main.rs index 66e8901..6501411 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,6 @@ use utils::{ }; mod arguments; -mod callback; mod chain; mod database; mod definitions; @@ -122,7 +121,7 @@ async fn async_try_main( let (task_tracker, error_rx) = TaskTracker::new(); let recipient = AccountId32::from_base58_string(&recipient_string) - .map_err(Error::RecipientAccount)? + .map_err(|e| Error::RecipientAccount(e.to_string()))? .0; let signer = Signer::init(recipient, task_tracker.clone(), seed_env_vars.seed)?; diff --git a/src/state.rs b/src/state.rs index 0de01aa..eb9f0ab 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,4 @@ +use crate::error::ForceWithdrawalError; use crate::{ chain::ChainManager, database::{ConfigWoChains, Database, TransactionInfoDb}, @@ -133,7 +134,17 @@ impl State { // Only perform actions if the record is saved in ledger match state.db.mark_paid(id.clone()).await { Ok(order) => { - // TODO: callback here + if !order.callback.is_empty() { + let callback = order.callback.clone(); + tokio::spawn(async move { + tracing::info!("Sending callback to: {}", callback); + + // fire and forget + if let Err(e) = reqwest::Client::new().get(&callback).send().await { + tracing::error!("Failed to send callback to {}: {:?}", callback, e); + } + }); + } drop(state.chain_manager.reap(id, order, state.recipient).await); } Err(e) => { @@ -150,6 +161,45 @@ impl State { ) } } + StateAccessRequest::OrderWithdrawn(id) => { + match state.db.mark_withdrawn(id.clone()).await { + Ok(order) => { + tracing::info!("Order {id} successfully marked as withdrawn"); + } + Err(e) => { + tracing::error!( + "Order was withdrawn but this could not be recorded! {e:?}" + ) + } + } + } + StateAccessRequest::ForceWithdrawal(id) => { + match state.db.read_order(id.clone()).await { + Ok(Some(order_info)) => { + match state.chain_manager.reap(id.clone(), order_info.clone(), state.recipient).await { + Ok(_) => { + match state.db.mark_forced(id.clone()).await { + Ok(_) => { + tracing::info!("Order {id} successfully marked as force withdrawn"); + } + Err(e) => { + tracing::error!("Failed to mark order {id} as forced: {e:?}"); + } + } + } + Err(e) => { + tracing::error!("Failed to initiate forced payout for order {id}: {e:?}"); + } + } + } + Ok(None) => { + tracing::error!("Order {id} not found in database"); + } + Err(e) => { + tracing::error!("Error reading order {id} from database: {e:?}"); + } + } + } }; } // Orchestrate shutdown from here @@ -269,16 +319,31 @@ impl State { tracing::warn!("Data race on shutdown; please restart the daemon for cleaning up"); }; } - - pub async fn add_transaction(&self, order: String, tx: String) { - todo!() + pub async fn order_withdrawn(&self, order: String) { + if self + .tx + .send(StateAccessRequest::OrderWithdrawn(order)) + .await + .is_err() + { + tracing::warn!("Data race on shutdown; please restart the daemon for cleaning up"); + }; } - #[allow(dead_code)] - pub async fn force_withdrawal(&self, order: String) -> Result { - todo!() - } + pub async fn force_withdrawal( + &self, + order: String, + ) -> Result { + self.tx + .send(StateAccessRequest::ForceWithdrawal(order.clone())) + .await + .map_err(|_| ForceWithdrawalError::InvalidParameter(order.clone()))?; + match self.order_status(&order).await { + Ok(order_status) => Ok(order_status), + Err(_) => Ok(OrderResponse::NotFound), + } + } pub fn interface(&self) -> Self { State { tx: self.tx.clone(), @@ -312,6 +377,8 @@ enum StateAccessRequest { order: String, tx: TransactionInfoDb, }, + OrderWithdrawn(String), + ForceWithdrawal(String), } struct GetInvoiceStatus { diff --git a/src/utils/logger.rs b/src/utils/logger.rs index 8c75112..2549e36 100644 --- a/src/utils/logger.rs +++ b/src/utils/logger.rs @@ -1,9 +1,8 @@ use super::shutdown; -use crate::{callback, database, error::Error, server}; +use crate::{database, error::Error, server}; use tracing_subscriber::{fmt::time::UtcTime, EnvFilter}; const TARGETS: &[&str] = &[ - callback::MODULE, database::MODULE, server::MODULE, shutdown::MODULE, diff --git a/tests/kalatori-api-test-suite/src/polkadot.ts b/tests/kalatori-api-test-suite/src/polkadot.ts index 9099023..2c51fab 100644 --- a/tests/kalatori-api-test-suite/src/polkadot.ts +++ b/tests/kalatori-api-test-suite/src/polkadot.ts @@ -47,5 +47,5 @@ export async function transferFunds(rpcUrl: string, paymentAccount: string, amou }); // Wait for transaction to be included in block - await new Promise(resolve => setTimeout(resolve, 8000)); + await new Promise(resolve => setTimeout(resolve, 10000)); } diff --git a/tests/kalatori-api-test-suite/tests/order.test.ts b/tests/kalatori-api-test-suite/tests/order.test.ts index 290edd2..2644e8f 100644 --- a/tests/kalatori-api-test-suite/tests/order.test.ts +++ b/tests/kalatori-api-test-suite/tests/order.test.ts @@ -8,7 +8,7 @@ describe('Order Endpoint Blackbox Tests', () => { throw new Error('check all environment variables are defined'); } const dotOrderData = { - amount: 1, + amount: 4, // Crucial to test with more than existential amount which is 1 DOT currency: 'DOT', callback: 'https://example.com/callback' }; @@ -216,7 +216,7 @@ describe('Order Endpoint Blackbox Tests', () => { expect(response.status).toBe(404); }); - it.skip('should create, repay, and automatically withdraw an order in DOT', async () => { + it('should create, repay, and automatically withdraw an order in DOT', async () => { const orderId = generateRandomOrderId(); await createOrder(orderId, dotOrderData); const orderDetails = await getOrderDetails(orderId); @@ -225,10 +225,13 @@ describe('Order Endpoint Blackbox Tests', () => { await transferFunds(orderDetails.currency.rpc_url, paymentAccount, dotOrderData.amount); + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 35000)); + const repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('paid'); expect(repaidOrderDetails.withdrawal_status).toBe('completed'); - }, 30000); + }, 100000); it.skip('should create, repay, and automatically withdraw an order in USDC', async () => { const orderId = generateRandomOrderId(); @@ -244,10 +247,13 @@ describe('Order Endpoint Blackbox Tests', () => { orderDetails.currency.asset_id ); + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + const repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('paid'); expect(repaidOrderDetails.withdrawal_status).toBe('completed'); - }, 30000); + }, 50000); it.skip('should not automatically withdraw an order until fully repaid', async () => { const orderId = generateRandomOrderId(); @@ -265,6 +271,9 @@ describe('Order Endpoint Blackbox Tests', () => { halfAmount, orderDetails.currency.asset_id ); + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + let repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('pending'); expect(repaidOrderDetails.withdrawal_status).toBe('waiting'); @@ -276,10 +285,14 @@ describe('Order Endpoint Blackbox Tests', () => { halfAmount, orderDetails.currency.asset_id ); + + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('paid'); expect(repaidOrderDetails.withdrawal_status).toBe('completed'); - }, 30000); + }, 50000); it.skip('should not update order if received payment in wrong currency', async () => { const orderId = generateRandomOrderId(); @@ -296,12 +309,40 @@ describe('Order Endpoint Blackbox Tests', () => { assetId ); + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + const repaidOrderDetails = await getOrderDetails(orderId); expect(repaidOrderDetails.payment_status).toBe('pending'); expect(repaidOrderDetails.withdrawal_status).toBe('waiting'); - }, 30000); + }, 50000); + + it('should be able to force withdraw partially repayed order', async () => { + const orderId = generateRandomOrderId(); + await createOrder(orderId, dotOrderData); + const orderDetails = await getOrderDetails(orderId); + const paymentAccount = orderDetails.payment_account; + expect(paymentAccount).toBeDefined(); + + await transferFunds(orderDetails.currency.rpc_url, paymentAccount, dotOrderData.amount/2); + + // lets wait for the changes to get propagated on chain and app to catch them + await new Promise(resolve => setTimeout(resolve, 15000)); + + const partiallyRepaidOrderDetails = await getOrderDetails(orderId); + expect(partiallyRepaidOrderDetails.payment_status).toBe('pending'); + expect(partiallyRepaidOrderDetails.withdrawal_status).toBe('waiting'); + + const response = await request(baseUrl) + .post(`/v2/order/${orderId}/forceWithdrawal`); + expect(response.status).toBe(201); + + let forcedOrderDetails = await getOrderDetails(orderId); + expect(forcedOrderDetails.payment_status).toBe('pending'); + expect(forcedOrderDetails.withdrawal_status).toBe('forced'); + }, 100000); - it.skip('should return 404 for non-existing order on force withdrawal', async () => { + it('should return 404 for non-existing order on force withdrawal', async () => { const nonExistingOrderId = 'nonExistingOrder123'; const response = await request(baseUrl) .post(`/v2/order/${nonExistingOrderId}/forceWithdrawal`);