diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 8156a6934b..467dc4ebc4 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/debian:10 -MAINTAINER Onur Özkan +LABEL authors="Onur Özkan " RUN apt-get update -y @@ -11,6 +11,7 @@ RUN apt-get install -y \ curl \ wget \ unzip \ + libudev-dev \ gnupg RUN ln -s /usr/bin/python3 /bin/python @@ -49,8 +50,9 @@ RUN apt-get install -y \ docker-buildx-plugin RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --profile minimal --default-toolchain nightly-2023-06-01 -y - -RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-linux-x86_64.zip -RUN unzip protoc-3.20.1-linux-x86_64.zip && mv ./include/google /usr/include/google - ENV PATH="/root/.cargo/bin:$PATH" +# TODO: Lock wasm-pack version +RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y +RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v25.3/protoc-25.3-linux-x86_64.zip +RUN unzip protoc-25.3-linux-x86_64.zip && mv ./include/google /usr/include/google + diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb deleted file mode 100644 index 5979f2fdb6..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/000122.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb deleted file mode 100644 index b03a2ddeb6..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/000123.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb deleted file mode 100644 index 415499ade2..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/000124.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb deleted file mode 100644 index 90619e3797..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/000125.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000126.log b/.docker/container-state/atom-testnet-data/data/application.db/000126.log deleted file mode 100644 index 69d85e6d61..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/000126.log and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000145.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000145.ldb new file mode 100644 index 0000000000..ffa94a328e Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000145.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000146.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000146.ldb new file mode 100644 index 0000000000..305c23033d Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000146.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000147.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000147.ldb new file mode 100644 index 0000000000..4c895dfd75 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000147.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000148.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000148.ldb new file mode 100644 index 0000000000..44b0a978d0 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000148.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000149.ldb b/.docker/container-state/atom-testnet-data/data/application.db/000149.ldb new file mode 100644 index 0000000000..a79ab5bddf Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000149.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/000150.log b/.docker/container-state/atom-testnet-data/data/application.db/000150.log new file mode 100644 index 0000000000..19f3ee4d7e Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/000150.log differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT index 224d52afe7..3f137c99d1 100644 --- a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT @@ -1 +1 @@ -MANIFEST-000127 +MANIFEST-000151 diff --git a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak index 2b465edb20..3e77273f6e 100644 --- a/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/application.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000121 +MANIFEST-000144 diff --git a/.docker/container-state/atom-testnet-data/data/application.db/LOG b/.docker/container-state/atom-testnet-data/data/application.db/LOG index 40eb63ea2c..0e35591395 100644 --- a/.docker/container-state/atom-testnet-data/data/application.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/application.db/LOG @@ -470,3 +470,79 @@ 17:59:01.710322 version@stat F·[1 3] S·6MiB[739KiB 5MiB] Sc·[0.25 0.06] 17:59:01.715180 db@janitor F·6 G·0 17:59:01.715196 db@open done T·17.361833ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.858576 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.858710 version@stat F·[1 3] S·6MiB[739KiB 5MiB] Sc·[0.25 0.06] +05:10:40.858723 db@open opening +05:10:40.858766 journal@recovery F·1 +05:10:40.858847 journal@recovery recovering @126 +05:10:40.860765 memdb@flush created L0@128 N·905 S·48KiB "s/713,v122410":"s/p..hts,v122409" +05:10:40.862727 version@stat F·[2 3] S·6MiB[788KiB 5MiB] Sc·[0.50 0.06] +05:10:40.865397 db@janitor F·7 G·0 +05:10:40.865410 db@open done T·6.680542ms +05:10:40.972856 table@compaction L0·2 -> L1·3 S·6MiB Q·123136 +05:10:40.990144 table@build created L1@131 N·26043 S·2MiB "s/1,v430":"s/k..Z\xe2w,v101980" +05:10:41.012132 table@build created L1@132 N·39970 S·2MiB "s/k..\x8e\xd2\x7f,v43561":"s/k..\x86S\xad,v103471" +05:10:41.027607 table@build created L1@133 N·18990 S·2MiB "s/k..\x82\xb9\x82,v24251":"s/k..\x1a\xf6y,v40651" +05:10:41.034441 table@build created L1@134 N·9675 S·376KiB "s/k..\x9b\xb8\x1d,v40639":"s/p..hts,v123133" +05:10:41.034500 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +05:10:41.035116 table@compaction committed F-1 S-28KiB Ke·0 D·3011 T·62.239547ms +05:10:41.035304 table@remove removed @125 +05:10:41.035687 table@remove removed @122 +05:10:41.036200 table@remove removed @123 +05:10:41.036511 table@remove removed @124 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.533830 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.533977 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.06] +06:44:58.533988 db@open opening +06:44:58.534025 journal@recovery F·1 +06:44:58.534163 journal@recovery recovering @129 +06:44:58.588185 memdb@flush created L0@135 N·26580 S·1MiB "s/718,v123316":"s/p..hts,v123315" +06:44:58.588321 version@stat F·[1 4] S·7MiB[1MiB 6MiB] Sc·[0.25 0.06] +06:44:58.609803 db@janitor F·8 G·1 +06:44:58.609810 db@janitor removing table-128 +06:44:58.609853 db@open done T·75.860223ms +06:45:28.811214 table@compaction L0·1 -> L1·4 S·7MiB Q·150786 +06:45:28.852586 table@build created L1@138 N·26523 S·2MiB "s/1,v430":"s/k..\xfaW\xa8,v68382" +06:45:28.897007 table@build created L1@139 N·36709 S·2MiB "s/k..>Uu,v62163":"s/k..\x00\x01\xa4,v70055" +06:45:28.935643 table@build created L1@140 N·27416 S·2MiB "s/k..\x00\x01\xa5,v70226":"s/k..i\xf8\x8a,v22133" +06:45:28.974020 table@build created L1@141 N·24774 S·1MiB "s/k..e^\xef,v12056":"s/p..hts,v149714" +06:45:28.974046 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:45:28.981030 table@compaction committed F-1 S-26KiB Ke·0 D·5836 T·169.796253ms +06:45:28.981494 table@remove removed @131 +06:45:28.981958 table@remove removed @132 +06:45:28.982402 table@remove removed @133 +06:45:28.982499 table@remove removed @134 +=============== Oct 17, 2024 (UTC) =============== +06:45:46.585282 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.585428 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:45:46.585445 db@open opening +06:45:46.585484 journal@recovery F·1 +06:45:46.585567 journal@recovery recovering @136 +06:45:46.587472 memdb@flush created L0@142 N·1427 S·80KiB "s/862,v149897":"s/p..hts,v149896" +06:45:46.588280 version@stat F·[1 4] S·7MiB[80KiB 7MiB] Sc·[0.25 0.08] +06:45:46.591569 db@janitor F·8 G·1 +06:45:46.591580 db@janitor removing table-135 +06:45:46.591873 db@open done T·6.422715ms +06:46:51.810714 table@compaction L0·1 -> L1·4 S·7MiB Q·153293 +06:46:51.853844 table@build created L1@145 N·26596 S·2MiB "s/1,v430":"s/k..R\xa9',v46917" +06:46:51.895701 table@build created L1@146 N·36341 S·2MiB "s/k..\xc8A\xba,v114547":"s/k..\xa2*\xbf,v100466" +06:46:51.941201 table@build created L1@147 N·28140 S·2MiB "s/k..\xee^\xb5,v100467":"s/k..\xabk`,v122744" +06:46:51.976205 table@build created L1@148 N·25470 S·1MiB "s/k..̵\xd4,v106125":"s/p..hts,v151142" +06:46:51.976239 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:46:51.983945 table@compaction committed F-1 S-1KiB Ke·0 D·302 T·173.208828ms +06:46:51.984409 table@remove removed @138 +06:46:51.984907 table@remove removed @139 +06:46:51.985406 table@remove removed @140 +06:46:51.985884 table@remove removed @141 +=============== Oct 17, 2024 (UTC) =============== +06:47:26.207760 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.207883 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:47:26.207895 db@open opening +06:47:26.207934 journal@recovery F·1 +06:47:26.208025 journal@recovery recovering @143 +06:47:26.210939 memdb@flush created L0@149 N·2685 S·147KiB "s/869,v151323":"s/p..hts,v151322" +06:47:26.211271 version@stat F·[1 4] S·8MiB[147KiB 7MiB] Sc·[0.25 0.08] +06:47:26.214624 db@janitor F·8 G·1 +06:47:26.214639 db@janitor removing table-142 +06:47:26.214702 db@open done T·6.802848ms diff --git a/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 b/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 deleted file mode 100644 index 28e3f506f3..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000127 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000151 b/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000151 new file mode 100644 index 0000000000..12bea3a66a Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/application.db/MANIFEST-000151 differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb deleted file mode 100644 index 8ddf4ae7fb..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/blockstore.db/000093.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb deleted file mode 100644 index 854fba8071..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/blockstore.db/000096.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log b/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log deleted file mode 100644 index b5adbff40d..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/blockstore.db/000097.log and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000109.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000109.ldb new file mode 100644 index 0000000000..f850f5a1ef Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000109.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000110.ldb b/.docker/container-state/atom-testnet-data/data/blockstore.db/000110.ldb new file mode 100644 index 0000000000..6587bbe2bb Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000110.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/000111.log b/.docker/container-state/atom-testnet-data/data/blockstore.db/000111.log new file mode 100644 index 0000000000..0b0f0c9313 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/000111.log differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT index 95395b28bd..b59a6ba248 100644 --- a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT @@ -1 +1 @@ -MANIFEST-000098 +MANIFEST-000112 diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak index b993e6cda5..db6fa61f96 100644 --- a/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000095 +MANIFEST-000108 diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG b/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG index 969bf1ea7a..8d007b4ffd 100644 --- a/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/blockstore.db/LOG @@ -384,3 +384,57 @@ 17:59:01.758668 version@stat F·[1 1] S·684KiB[72KiB 611KiB] Sc·[0.25 0.01] 17:59:01.761454 db@janitor F·4 G·0 17:59:01.761470 db@open done T·5.254513ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.909640 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.909735 version@stat F·[1 1] S·684KiB[72KiB 611KiB] Sc·[0.25 0.01] +05:10:40.909748 db@open opening +05:10:40.909778 journal@recovery F·1 +05:10:40.909866 journal@recovery recovering @97 +05:10:40.910854 memdb@flush created L0@99 N·30 S·3KiB "BH:..eed,v4304":"blo..ore,v4301" +05:10:40.911054 version@stat F·[2 1] S·688KiB[76KiB 611KiB] Sc·[0.50 0.01] +05:10:40.915558 db@janitor F·5 G·0 +05:10:40.915581 db@open done T·5.827674ms +05:11:03.981547 table@compaction L0·2 -> L1·1 S·688KiB Q·4350 +05:11:03.987164 table@build created L1@102 N·3586 S·689KiB "BH:..c56,v1346":"blo..ore,v4325" +05:11:03.987190 version@stat F·[0 1] S·689KiB[0B 689KiB] Sc·[0.00 0.01] +05:11:03.987766 table@compaction committed F-2 S+1KiB Ke·0 D·74 T·6.185418ms +05:11:03.987844 table@remove removed @96 +05:11:03.987972 table@remove removed @93 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.661274 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.661360 version@stat F·[0 1] S·689KiB[0B 689KiB] Sc·[0.00 0.01] +06:44:58.661370 db@open opening +06:44:58.661399 journal@recovery F·1 +06:44:58.661518 journal@recovery recovering @100 +06:44:58.681937 memdb@flush created L0@103 N·864 S·144KiB "BH:..573,v4461":"blo..ore,v4332" +06:44:58.682057 version@stat F·[1 1] S·834KiB[144KiB 689KiB] Sc·[0.25 0.01] +06:44:58.690133 db@janitor F·5 G·1 +06:44:58.690138 db@janitor removing table-99 +06:44:58.690162 db@open done T·28.789108ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.638683 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.638799 version@stat F·[1 1] S·834KiB[144KiB 689KiB] Sc·[0.25 0.01] +06:45:46.638816 db@open opening +06:45:46.638854 journal@recovery F·1 +06:45:46.638937 journal@recovery recovering @104 +06:45:46.639741 memdb@flush created L0@106 N·42 S·10KiB "BH:..d0b,v5194":"blo..ore,v5197" +06:45:46.639858 version@stat F·[2 1] S·844KiB[154KiB 689KiB] Sc·[0.50 0.01] +06:45:46.642453 db@janitor F·5 G·0 +06:45:46.642465 db@open done T·3.643621ms +06:46:17.600570 table@compaction L0·2 -> L1·1 S·844KiB Q·5270 +06:46:17.605912 table@build created L1@109 N·4341 S·845KiB "BH:..c56,v1346":"blo..ore,v5233" +06:46:17.605934 version@stat F·[0 1] S·845KiB[0B 845KiB] Sc·[0.00 0.01] +06:46:17.607466 table@compaction committed F-2 S+552B Ke·0 D·151 T·6.876138ms +06:46:17.607551 table@remove removed @103 +06:46:17.607706 table@remove removed @102 +=============== Oct 17, 2024 (UTC) =============== +06:47:26.263473 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.263569 version@stat F·[0 1] S·845KiB[0B 845KiB] Sc·[0.00 0.01] +06:47:26.263581 db@open opening +06:47:26.263610 journal@recovery F·1 +06:47:26.263695 journal@recovery recovering @107 +06:47:26.264737 memdb@flush created L0@110 N·90 S·14KiB "BH:..5c8,v5321":"blo..ore,v5240" +06:47:26.264916 version@stat F·[1 1] S·859KiB[14KiB 845KiB] Sc·[0.25 0.01] +06:47:26.269660 db@janitor F·5 G·1 +06:47:26.269667 db@janitor removing table-106 +06:47:26.269704 db@open done T·6.119582ms diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 b/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 deleted file mode 100644 index a7d73d0ef2..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000098 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000112 b/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000112 new file mode 100644 index 0000000000..07d83114fb Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/blockstore.db/MANIFEST-000112 differ diff --git a/.docker/container-state/atom-testnet-data/data/cs.wal/wal b/.docker/container-state/atom-testnet-data/data/cs.wal/wal index 61a997fc44..58106e91fa 100644 Binary files a/.docker/container-state/atom-testnet-data/data/cs.wal/wal and b/.docker/container-state/atom-testnet-data/data/cs.wal/wal differ diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/000068.log b/.docker/container-state/atom-testnet-data/data/evidence.db/000076.log similarity index 100% rename from .docker/container-state/atom-testnet-data/data/evidence.db/000068.log rename to .docker/container-state/atom-testnet-data/data/evidence.db/000076.log diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT index 5893b8f83b..c7a124bfc3 100644 --- a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT @@ -1 +1 @@ -MANIFEST-000069 +MANIFEST-000077 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak index 0094dacbb8..d2ea14ced0 100644 --- a/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000067 +MANIFEST-000075 diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/LOG b/.docker/container-state/atom-testnet-data/data/evidence.db/LOG index 5ab563f5bc..e4d4d0631a 100644 --- a/.docker/container-state/atom-testnet-data/data/evidence.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/evidence.db/LOG @@ -310,3 +310,39 @@ 17:59:01.774745 version@stat F·[] S·0B[] Sc·[] 17:59:01.777793 db@janitor F·2 G·0 17:59:01.777805 db@open done T·4.373546ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.928961 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.929028 version@stat F·[] S·0B[] Sc·[] +05:10:40.929037 db@open opening +05:10:40.929066 journal@recovery F·1 +05:10:40.929428 journal@recovery recovering @68 +05:10:40.931225 version@stat F·[] S·0B[] Sc·[] +05:10:40.934658 db@janitor F·2 G·0 +05:10:40.934670 db@open done T·5.628153ms +=============== Oct 17, 2024 (UTC) =============== +06:44:58.730742 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.730819 version@stat F·[] S·0B[] Sc·[] +06:44:58.730838 db@open opening +06:44:58.730866 journal@recovery F·1 +06:44:58.732739 journal@recovery recovering @70 +06:44:58.734542 version@stat F·[] S·0B[] Sc·[] +06:44:58.749764 db@janitor F·2 G·0 +06:44:58.749779 db@open done T·18.938744ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.656897 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.656961 version@stat F·[] S·0B[] Sc·[] +06:45:46.656972 db@open opening +06:45:46.656996 journal@recovery F·1 +06:45:46.657075 journal@recovery recovering @72 +06:45:46.657200 version@stat F·[] S·0B[] Sc·[] +06:45:46.659741 db@janitor F·2 G·0 +06:45:46.659750 db@open done T·2.774794ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.282862 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.282927 version@stat F·[] S·0B[] Sc·[] +06:47:26.282937 db@open opening +06:47:26.282964 journal@recovery F·1 +06:47:26.283046 journal@recovery recovering @74 +06:47:26.283185 version@stat F·[] S·0B[] Sc·[] +06:47:26.285765 db@janitor F·2 G·0 +06:47:26.285779 db@open done T·2.837874ms diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 b/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 deleted file mode 100644 index 3833569575..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000069 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000077 b/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000077 new file mode 100644 index 0000000000..dd7fb6cd3d Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/evidence.db/MANIFEST-000077 differ diff --git a/.docker/container-state/atom-testnet-data/data/priv_validator_state.json b/.docker/container-state/atom-testnet-data/data/priv_validator_state.json index 99546706e3..56899e7a9a 100644 --- a/.docker/container-state/atom-testnet-data/data/priv_validator_state.json +++ b/.docker/container-state/atom-testnet-data/data/priv_validator_state.json @@ -1,7 +1,7 @@ { - "height": "717", + "height": "888", "round": 0, "step": 3, - "signature": "XLwGIxJpiVU0AYl646G7zqXk0v/ihlfAwKP5BvHmMfy6UQCzeW/rjtuVoS+2i7KT0RVUfXc7dvGYOnpuAvSTAA==", - "signbytes": "76080211CD0200000000000022480A20911EABA0078B62D4B55F3B349F0719F53B0D39EEE4064EA626437B86D219A37F122408011220AB2248EBDDC8ACA32BCF90D60E19DA9B4A42F2DD6A8957FA3D7337C1AC73151A2A0C08FEA296B40610F7CFDA82033211636F736D6F736875622D746573746E6574" + "signature": "XskRDe70STOW5qxh60WQ2GR/2x2sDaGqPnAsTb7icGjekGky1w7kO/b3nU+XzmQVDaQcYHjb8W1JRO4ue9TeAA==", + "signbytes": "76080211780300000000000022480A20F5676C89CEA03B617C53FCD05015F465FB2C14107310553F186D423A022E05E2122408011220AA62CEBCC34EFE61C30D5917935B2224E39D5019F92CE313CB8514F584F6D54E2A0C0897E6C2B80610EBD7E698013211636F736D6F736875622D746573746E6574" } \ No newline at end of file diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000068.log b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000076.log similarity index 100% rename from .docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000068.log rename to .docker/container-state/atom-testnet-data/data/snapshots/metadata.db/000076.log diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT index 5893b8f83b..c7a124bfc3 100644 --- a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT @@ -1 +1 @@ -MANIFEST-000069 +MANIFEST-000077 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak index 0094dacbb8..d2ea14ced0 100644 --- a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000067 +MANIFEST-000075 diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG index 2638185812..108e3e10d4 100644 --- a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/LOG @@ -310,3 +310,39 @@ 17:59:01.716921 version@stat F·[] S·0B[] Sc·[] 17:59:01.720432 db@janitor F·2 G·0 17:59:01.720442 db@open done T·3.774542ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.866907 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.866974 version@stat F·[] S·0B[] Sc·[] +05:10:40.866984 db@open opening +05:10:40.867009 journal@recovery F·1 +05:10:40.867092 journal@recovery recovering @68 +05:10:40.867215 version@stat F·[] S·0B[] Sc·[] +05:10:40.871101 db@janitor F·2 G·0 +05:10:40.871109 db@open done T·4.121771ms +=============== Oct 17, 2024 (UTC) =============== +06:44:58.611297 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.611385 version@stat F·[] S·0B[] Sc·[] +06:44:58.611394 db@open opening +06:44:58.611420 journal@recovery F·1 +06:44:58.611708 journal@recovery recovering @70 +06:44:58.613594 version@stat F·[] S·0B[] Sc·[] +06:44:58.621731 db@janitor F·2 G·0 +06:44:58.621742 db@open done T·10.34492ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.593245 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.593314 version@stat F·[] S·0B[] Sc·[] +06:45:46.593325 db@open opening +06:45:46.593352 journal@recovery F·1 +06:45:46.595203 journal@recovery recovering @72 +06:45:46.597207 version@stat F·[] S·0B[] Sc·[] +06:45:46.599821 db@janitor F·2 G·0 +06:45:46.599833 db@open done T·6.503955ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.215962 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.216036 version@stat F·[] S·0B[] Sc·[] +06:47:26.216047 db@open opening +06:47:26.216083 journal@recovery F·1 +06:47:26.217966 journal@recovery recovering @74 +06:47:26.220147 version@stat F·[] S·0B[] Sc·[] +06:47:26.222797 db@janitor F·2 G·0 +06:47:26.222807 db@open done T·6.756568ms diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 deleted file mode 100644 index 3833569575..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000069 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000077 b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000077 new file mode 100644 index 0000000000..dd7fb6cd3d Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/snapshots/metadata.db/MANIFEST-000077 differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb b/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb deleted file mode 100644 index fb954e1eb4..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/state.db/000114.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000129.ldb b/.docker/container-state/atom-testnet-data/data/state.db/000129.ldb new file mode 100644 index 0000000000..b38bc5d97d Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/000129.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000115.log b/.docker/container-state/atom-testnet-data/data/state.db/000130.log similarity index 68% rename from .docker/container-state/atom-testnet-data/data/state.db/000115.log rename to .docker/container-state/atom-testnet-data/data/state.db/000130.log index 5eec6803eb..bbb9c9ebd2 100644 Binary files a/.docker/container-state/atom-testnet-data/data/state.db/000115.log and b/.docker/container-state/atom-testnet-data/data/state.db/000130.log differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/000113.ldb b/.docker/container-state/atom-testnet-data/data/state.db/000132.ldb similarity index 63% rename from .docker/container-state/atom-testnet-data/data/state.db/000113.ldb rename to .docker/container-state/atom-testnet-data/data/state.db/000132.ldb index 4775c1b3ee..e589a7bb5d 100644 Binary files a/.docker/container-state/atom-testnet-data/data/state.db/000113.ldb and b/.docker/container-state/atom-testnet-data/data/state.db/000132.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT index 2b6390e186..d5731e9f98 100644 --- a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT @@ -1 +1 @@ -MANIFEST-000116 +MANIFEST-000131 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak index b59a6ba248..224d52afe7 100644 --- a/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/state.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000112 +MANIFEST-000127 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/LOG b/.docker/container-state/atom-testnet-data/data/state.db/LOG index 2c9dc925a4..333f86a5c7 100644 --- a/.docker/container-state/atom-testnet-data/data/state.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/state.db/LOG @@ -441,3 +441,67 @@ 17:59:01.766489 db@janitor F·5 G·1 17:59:01.766499 db@janitor removing table-110 17:59:01.766531 db@open done T·4.87064ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.915711 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.915810 version@stat F·[1 1] S·327KiB[83KiB 243KiB] Sc·[0.25 0.00] +05:10:40.915825 db@open opening +05:10:40.915868 journal@recovery F·1 +05:10:40.918284 journal@recovery recovering @115 +05:10:40.919542 memdb@flush created L0@117 N·26 S·5KiB "abc..713,v3635":"val..719,v3657" +05:10:40.919668 version@stat F·[2 1] S·332KiB[88KiB 243KiB] Sc·[0.50 0.00] +05:10:40.923785 db@janitor F·5 G·0 +05:10:40.923796 db@open done T·7.966161ms +05:11:00.958934 table@compaction L0·2 -> L1·1 S·332KiB Q·3676 +05:11:00.962394 table@build created L1@120 N·2158 S·280KiB "abc..y:1,v7":"val..:99,v491" +05:11:00.962424 version@stat F·[0 1] S·280KiB[0B 280KiB] Sc·[0.00 0.00] +05:11:00.964049 table@compaction committed F-2 S-51KiB Ke·0 D·150 T·5.093449ms +05:11:00.964119 table@remove removed @114 +05:11:00.964186 table@remove removed @113 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.690228 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.690292 version@stat F·[0 1] S·280KiB[0B 280KiB] Sc·[0.00 0.00] +06:44:58.690301 db@open opening +06:44:58.690327 journal@recovery F·1 +06:44:58.690411 journal@recovery recovering @118 +06:44:58.698458 memdb@flush created L0@121 N·721 S·152KiB "abc..718,v3662":"val..863,v4379" +06:44:58.698595 version@stat F·[1 1] S·433KiB[152KiB 280KiB] Sc·[0.25 0.00] +06:44:58.713491 db@janitor F·5 G·1 +06:44:58.713496 db@janitor removing table-117 +06:44:58.713521 db@open done T·23.21798ms +06:45:33.812957 table@compaction L0·1 -> L1·1 S·433KiB Q·4413 +06:45:33.817194 table@build created L1@124 N·2590 S·345KiB "abc..y:1,v7":"val..:99,v491" +06:45:33.817219 version@stat F·[0 1] S·345KiB[0B 345KiB] Sc·[0.00 0.00] +06:45:33.818740 table@compaction committed F-1 S-88KiB Ke·0 D·289 T·5.758129ms +06:45:33.818856 table@remove removed @120 +=============== Oct 17, 2024 (UTC) =============== +06:45:46.642590 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.642660 version@stat F·[0 1] S·345KiB[0B 345KiB] Sc·[0.00 0.00] +06:45:46.642670 db@open opening +06:45:46.642701 journal@recovery F·1 +06:45:46.642793 journal@recovery recovering @122 +06:45:46.643923 memdb@flush created L0@125 N·36 S·14KiB "abc..862,v4384":"val..870,v4416" +06:45:46.644184 version@stat F·[1 1] S·359KiB[14KiB 345KiB] Sc·[0.25 0.00] +06:45:46.648159 db@janitor F·5 G·1 +06:45:46.648166 db@janitor removing table-121 +06:45:46.648229 db@open done T·5.554767ms +06:46:12.624281 table@compaction L0·1 -> L1·1 S·359KiB Q·4445 +06:46:12.628139 table@build created L1@128 N·2611 S·351KiB "abc..y:1,v7":"val..:99,v491" +06:46:12.628162 version@stat F·[0 1] S·351KiB[0B 351KiB] Sc·[0.00 0.00] +06:46:12.628755 table@compaction committed F-1 S-7KiB Ke·0 D·15 T·4.453088ms +06:46:12.628879 table@remove removed @124 +=============== Oct 17, 2024 (UTC) =============== +06:47:26.269795 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.269871 version@stat F·[0 1] S·351KiB[0B 351KiB] Sc·[0.00 0.00] +06:47:26.269882 db@open opening +06:47:26.269915 journal@recovery F·1 +06:47:26.270011 journal@recovery recovering @126 +06:47:26.270915 memdb@flush created L0@129 N·76 S·13KiB "abc..869,v4421":"val..885,v4493" +06:47:26.271251 version@stat F·[1 1] S·364KiB[13KiB 351KiB] Sc·[0.25 0.00] +06:47:26.274125 db@janitor F·5 G·1 +06:47:26.274133 db@janitor removing table-125 +06:47:26.274169 db@open done T·4.283147ms +06:47:51.337900 table@compaction L0·1 -> L1·1 S·364KiB Q·4522 +06:47:51.342188 table@build created L1@132 N·2656 S·356KiB "abc..y:1,v7":"val..:99,v491" +06:47:51.342209 version@stat F·[0 1] S·356KiB[0B 356KiB] Sc·[0.00 0.00] +06:47:51.343789 table@compaction committed F-1 S-7KiB Ke·0 D·31 T·5.87117ms +06:47:51.343944 table@remove removed @128 diff --git a/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 b/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 deleted file mode 100644 index 41565f0c98..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000116 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000131 b/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000131 new file mode 100644 index 0000000000..f5239ae69c Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/state.db/MANIFEST-000131 differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb deleted file mode 100644 index 13cc2bd346..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/000070.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb deleted file mode 100644 index c7dcbe51d4..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/000071.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb deleted file mode 100644 index 32bb2004b9..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/000090.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb deleted file mode 100644 index f22e4b3e9b..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/000095.ldb and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log b/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log deleted file mode 100644 index a57e507e95..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/000096.log and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000101.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000101.ldb new file mode 100644 index 0000000000..708a3e9b1f Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000101.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000102.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000102.ldb new file mode 100644 index 0000000000..881b5395e2 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000102.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000105.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000105.ldb new file mode 100644 index 0000000000..f6ef7556d8 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000105.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000108.ldb b/.docker/container-state/atom-testnet-data/data/tx_index.db/000108.ldb new file mode 100644 index 0000000000..5e5fa0b9d8 Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000108.ldb differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/000109.log b/.docker/container-state/atom-testnet-data/data/tx_index.db/000109.log new file mode 100644 index 0000000000..d42d24bdef Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/000109.log differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT index af00d34b29..a451d53d04 100644 --- a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT @@ -1 +1 @@ -MANIFEST-000097 +MANIFEST-000110 diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak index 0ab25fa07f..aecd689375 100644 --- a/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000094 +MANIFEST-000107 diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG b/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG index 58ce5d3c39..30dd8dbfc5 100644 --- a/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG +++ b/.docker/container-state/atom-testnet-data/data/tx_index.db/LOG @@ -377,3 +377,52 @@ 17:59:01.769561 version@stat F·[3 1] S·414KiB[61KiB 352KiB] Sc·[0.75 0.00] 17:59:01.773003 db@janitor F·6 G·0 17:59:01.773016 db@open done T·5.882999ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.924218 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.924296 version@stat F·[3 1] S·414KiB[61KiB 352KiB] Sc·[0.75 0.00] +05:10:40.924308 db@open opening +05:10:40.924344 journal@recovery F·1 +05:10:40.924448 journal@recovery recovering @96 +05:10:40.925246 memdb@flush created L0@98 N·145 S·2KiB "blo..\x01\xc2\xc9,v21136":"blo..\x00\x01\xb9,v21262" +05:10:40.925383 version@stat F·[4 1] S·417KiB[64KiB 352KiB] Sc·[1.00 0.00] +05:10:40.928590 db@janitor F·7 G·0 +05:10:40.928600 db@open done T·4.288823ms +05:10:40.928646 table@compaction L0·4 -> L1·1 S·417KiB Q·21281 +05:10:40.938446 table@build created L1@101 N·21257 S·415KiB "\x04\x92\xf2..\v,\x8e,v12512":"\xef\xb9<..\x9a\x1b\x86,v4043" +05:10:40.938478 version@stat F·[0 1] S·415KiB[0B 415KiB] Sc·[0.00 0.00] +05:10:40.939072 table@compaction committed F-4 S-1KiB Ke·0 D·0 T·10.406849ms +05:10:40.939148 table@remove removed @95 +05:10:40.939176 table@remove removed @90 +05:10:40.939200 table@remove removed @71 +05:10:40.939286 table@remove removed @70 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.713909 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.714036 version@stat F·[0 1] S·415KiB[0B 415KiB] Sc·[0.00 0.00] +06:44:58.714046 db@open opening +06:44:58.714080 journal@recovery F·1 +06:44:58.714171 journal@recovery recovering @99 +06:44:58.717466 memdb@flush created L0@102 N·4372 S·103KiB "!\xe6\x14..CN\x88,v25566":"\xf8@\xbc..Ï\xfe,v23666" +06:44:58.717707 version@stat F·[1 1] S·518KiB[103KiB 415KiB] Sc·[0.25 0.00] +06:44:58.730090 db@janitor F·5 G·1 +06:44:58.730096 db@janitor removing table-98 +06:44:58.730122 db@open done T·16.072878ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.648667 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.648738 version@stat F·[1 1] S·518KiB[103KiB 415KiB] Sc·[0.25 0.00] +06:45:46.648749 db@open opening +06:45:46.648777 journal@recovery F·1 +06:45:46.650719 journal@recovery recovering @103 +06:45:46.651755 memdb@flush created L0@105 N·264 S·12KiB "blo..\x01\xc3^,v25655":"\xa0i\xe8..f\x92\x12,v25726" +06:45:46.653729 version@stat F·[2 1] S·531KiB[116KiB 415KiB] Sc·[0.50 0.00] +06:45:46.656293 db@janitor F·5 G·0 +06:45:46.656305 db@open done T·7.551795ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.274594 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.274675 version@stat F·[2 1] S·531KiB[116KiB 415KiB] Sc·[0.50 0.00] +06:47:26.274686 db@open opening +06:47:26.274721 journal@recovery F·1 +06:47:26.276698 journal@recovery recovering @106 +06:47:26.277708 memdb@flush created L0@108 N·435 S·7KiB "blo..\x01\xc3e,v25920":"blo..\x01\xc0\xbb,v26336" +06:47:26.279711 version@stat F·[3 1] S·538KiB[123KiB 415KiB] Sc·[0.75 0.00] +06:47:26.282380 db@janitor F·6 G·0 +06:47:26.282394 db@open done T·7.703855ms diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 b/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 deleted file mode 100644 index bbde422037..0000000000 Binary files a/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000097 and /dev/null differ diff --git a/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000110 b/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000110 new file mode 100644 index 0000000000..422b4dc63e Binary files /dev/null and b/.docker/container-state/atom-testnet-data/data/tx_index.db/MANIFEST-000110 differ diff --git a/.docker/container-state/ibc-relayer-data/config/config.yaml b/.docker/container-state/ibc-relayer-data/config/config.yaml index b31a94b30c..77886d2f40 100755 --- a/.docker/container-state/ibc-relayer-data/config/config.yaml +++ b/.docker/container-state/ibc-relayer-data/config/config.yaml @@ -10,12 +10,13 @@ chains: atom: type: cosmos value: - key-directory: /home/nimda/.relayer/keys/cosmoshub-testnet + key-directory: /root/.relayer/keys/cosmoshub-testnet key: test2 chain-id: cosmoshub-testnet rpc-addr: http://127.0.0.1:26658 account-prefix: cosmos keyring-backend: test + dynamic-gas-price: false gas-adjustment: 1.8 gas-prices: 0.5uatom min-gas-amount: 0 @@ -35,12 +36,13 @@ chains: nucleus: type: cosmos value: - key-directory: /home/nimda/.relayer/keys/nucleus-testnet + key-directory: /root/.relayer/keys/nucleus-testnet key: test1 chain-id: nucleus-testnet rpc-addr: http://127.0.0.1:26657 account-prefix: nuc keyring-backend: test + dynamic-gas-price: false gas-adjustment: 1.8 gas-prices: 0.5unucl min-gas-amount: 0 @@ -61,12 +63,12 @@ paths: nucleus-atom: src: chain-id: nucleus-testnet - client-id: 07-tendermint-1 - connection-id: connection-1 + client-id: 07-tendermint-2 + connection-id: connection-2 dst: chain-id: cosmoshub-testnet - client-id: 07-tendermint-1 - connection-id: connection-1 + client-id: 07-tendermint-2 + connection-id: connection-2 src-channel-filter: rule: "" channel-list: [] diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01059173227530771155 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01059173227530771155 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-01059173227530771155 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02330869765942450674 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02330869765942450674 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02330869765942450674 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02840196522853014193 b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02840196522853014193 new file mode 100644 index 0000000000..256a04df6e --- /dev/null +++ b/.docker/container-state/nucleus-testnet-data/config/write-file-atomic-02840196522853014193 @@ -0,0 +1,4 @@ +{ + "key": "f431aeec40157170c0988400", + "addrs": [] +} \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb deleted file mode 100644 index 626e16207a..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000124.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb deleted file mode 100644 index 3590e1577d..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000125.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb deleted file mode 100644 index c9145d6ba1..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000126.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb deleted file mode 100644 index d35ee8f42a..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000127.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb deleted file mode 100644 index 70efc2fb72..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000130.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log b/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log deleted file mode 100644 index 84e0725816..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/000131.log and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000154.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000154.ldb new file mode 100644 index 0000000000..a8a9d5d14a Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000154.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000155.log b/.docker/container-state/nucleus-testnet-data/data/application.db/000155.log new file mode 100644 index 0000000000..eb4ce2123b Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000155.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000157.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000157.ldb new file mode 100644 index 0000000000..d2efabcda6 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000157.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000158.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000158.ldb new file mode 100644 index 0000000000..0f54f43bcb Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000158.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000159.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000159.ldb new file mode 100644 index 0000000000..cb9fafd62b Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000159.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/000160.ldb b/.docker/container-state/nucleus-testnet-data/data/application.db/000160.ldb new file mode 100644 index 0000000000..a8417c6b23 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/000160.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT index c39c670731..c16f179ffd 100644 --- a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT @@ -1 +1 @@ -MANIFEST-000132 +MANIFEST-000156 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak index ea072ca932..1dec270d08 100644 --- a/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000129 +MANIFEST-000149 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/LOG b/.docker/container-state/nucleus-testnet-data/data/application.db/LOG index a806f60292..d5977bfc9f 100644 --- a/.docker/container-state/nucleus-testnet-data/data/application.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/application.db/LOG @@ -461,3 +461,91 @@ 17:59:01.664278 version@stat F·[1 4] S·6MiB[526KiB 6MiB] Sc·[0.25 0.06] 17:59:01.667135 db@janitor F·7 G·0 17:59:01.667155 db@open done T·11.159872ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.905554 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.905681 version@stat F·[1 4] S·6MiB[526KiB 6MiB] Sc·[0.25 0.06] +05:10:40.905693 db@open opening +05:10:40.905729 journal@recovery F·1 +05:10:40.905932 journal@recovery recovering @131 +05:10:40.907646 memdb@flush created L0@133 N·625 S·34KiB "s/1032,v129146":"s/p..hts,v129145" +05:10:40.907787 version@stat F·[2 4] S·6MiB[560KiB 6MiB] Sc·[0.50 0.06] +05:10:40.912478 db@janitor F·8 G·0 +05:10:40.912492 db@open done T·6.794072ms +05:10:40.980773 table@compaction L0·2 -> L1·4 S·6MiB Q·129648 +05:10:40.997373 table@build created L1@136 N·24265 S·2MiB "s/1,v499":"s/k..\xbb\x1da,v114109" +05:10:41.023402 table@build created L1@137 N·42517 S·2MiB "s/k..\v\xd1\xd9,v114104":"s/k..\x926\x81,v45485" +05:10:41.037226 table@build created L1@138 N·15792 S·2MiB "s/k..\xce\t\xe8,v127197":"s/k..A\xb8j,v10829" +05:10:41.043103 table@build created L1@139 N·14633 S·724KiB "s/k..\x0f\xbb',v65315":"s/p..hts,v129645" +05:10:41.043137 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.07] +05:10:41.043726 table@compaction committed F-2 S-8KiB Ke·0 D·2346 T·62.926263ms +05:10:41.043888 table@remove removed @130 +05:10:41.044256 table@remove removed @124 +05:10:41.044737 table@remove removed @125 +05:10:41.045200 table@remove removed @126 +05:10:41.045256 table@remove removed @127 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.635757 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.635904 version@stat F·[0 4] S·6MiB[0B 6MiB] Sc·[0.00 0.07] +06:44:58.635919 db@open opening +06:44:58.635952 journal@recovery F·1 +06:44:58.636043 journal@recovery recovering @134 +06:44:58.675589 memdb@flush created L0@140 N·18960 S·1MiB "s/1037,v129772":"s/p..hts,v129771" +06:44:58.675735 version@stat F·[1 4] S·7MiB[1MiB 6MiB] Sc·[0.25 0.07] +06:44:58.690561 db@janitor F·8 G·1 +06:44:58.690567 db@janitor removing table-133 +06:44:58.690608 db@open done T·54.685771ms +06:45:23.851285 table@compaction L0·1 -> L1·4 S·7MiB Q·149292 +06:45:23.863708 table@build created L1@143 N·20853 S·2MiB "s/1,v499":"s/k..\x90\xb6\xf1,v46843" +06:45:23.878194 table@build created L1@144 N·34793 S·2MiB "s/k..^\xb3\xfc,v46849":"s/k..\xf5X*,v124880" +06:45:23.891282 table@build created L1@145 N·32649 S·2MiB "s/k..\\\x8d?,v124877":"s/k..\xf6OO,v108777" +06:45:23.901184 table@build created L1@146 N·23324 S·1MiB "s/k..\xbe\x90\x84,v72822":"s/p..hts,v148606" +06:45:23.901206 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:45:23.901783 table@compaction committed F-1 S-22KiB Ke·0 D·4548 T·50.479285ms +06:45:23.902188 table@remove removed @136 +06:45:23.902594 table@remove removed @137 +06:45:23.903127 table@remove removed @138 +06:45:23.903277 table@remove removed @139 +=============== Oct 17, 2024 (UTC) =============== +06:45:46.632694 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.632829 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:45:46.632841 db@open opening +06:45:46.632873 journal@recovery F·1 +06:45:46.633065 journal@recovery recovering @141 +06:45:46.634967 memdb@flush created L0@147 N·1058 S·59KiB "s/1181,v148733":"s/p..hts,v148732" +06:45:46.635106 version@stat F·[1 4] S·7MiB[59KiB 7MiB] Sc·[0.25 0.08] +06:45:46.637686 db@janitor F·8 G·1 +06:45:46.637692 db@janitor removing table-140 +06:45:46.637923 db@open done T·5.078403ms +06:46:51.996792 table@compaction L0·1 -> L1·4 S·7MiB Q·151169 +06:46:52.036950 table@build created L1@150 N·20616 S·2MiB "s/1,v499":"s/k..w\x17D,v41895" +06:46:52.085978 table@build created L1@151 N·34742 S·2MiB "s/k..\x02F\xf2,v41900":"s/k..\xffQ{,v117451" +06:46:52.135406 table@build created L1@152 N·33250 S·2MiB "s/k..\x96\xeeu,v117450":"s/k..\x1c\xb9t,v75679" +06:46:52.173716 table@build created L1@153 N·23832 S·1MiB "s/k..{\x9f\xd9,v41367":"s/p..hts,v149665" +06:46:52.173757 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:46:52.181047 table@compaction committed F-1 S+1KiB Ke·0 D·237 T·184.227992ms +06:46:52.181530 table@remove removed @143 +06:46:52.182010 table@remove removed @144 +06:46:52.182474 table@remove removed @145 +06:46:52.182906 table@remove removed @146 +=============== Oct 17, 2024 (UTC) =============== +06:47:26.253548 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.253681 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:47:26.253693 db@open opening +06:47:26.253724 journal@recovery F·1 +06:47:26.253926 journal@recovery recovering @148 +06:47:26.256527 memdb@flush created L0@154 N·1873 S·100KiB "s/1188,v149792":"s/p..hts,v149791" +06:47:26.256677 version@stat F·[1 4] S·7MiB[100KiB 7MiB] Sc·[0.25 0.08] +06:47:26.259838 db@janitor F·8 G·1 +06:47:26.259845 db@janitor removing table-147 +06:47:26.259895 db@open done T·6.196873ms +06:47:51.346605 table@compaction L0·1 -> L1·4 S·7MiB Q·152042 +06:47:51.358754 table@build created L1@157 N·20319 S·2MiB "s/1,v499":"s/k..7\xcc\xf2,v36191" +06:47:51.372848 table@build created L1@158 N·34591 S·2MiB "s/k..8\x00\x1e,v36192":"s/k..\xfd<\x97,v106679" +06:47:51.386025 table@build created L1@159 N·34293 S·2MiB "s/k..\xe4\x8ej,v106681":"s/k..@m\xdb,v129528" +06:47:51.396388 table@build created L1@160 N·24643 S·1MiB "s/k..\xdb\xee\x8f,v145753":"s/p..hts,v151539" +06:47:51.396408 version@stat F·[0 4] S·7MiB[0B 7MiB] Sc·[0.00 0.08] +06:47:51.396980 table@compaction committed F-1 S-1KiB Ke·0 D·467 T·50.358509ms +06:47:51.397411 table@remove removed @150 +06:47:51.397858 table@remove removed @151 +06:47:51.398295 table@remove removed @152 +06:47:51.398626 table@remove removed @153 diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 b/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 deleted file mode 100644 index 50463c21d5..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000132 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000156 b/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000156 new file mode 100644 index 0000000000..7da12a9d2f Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/application.db/MANIFEST-000156 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb deleted file mode 100644 index 139161de2b..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000089.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log deleted file mode 100644 index 39218b184a..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000090.log and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb deleted file mode 100644 index f0670e0093..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000092.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000104.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000104.ldb new file mode 100644 index 0000000000..c4e0ac3f50 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000104.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000105.log b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000105.log new file mode 100644 index 0000000000..b8f80b5b12 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000105.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000107.ldb b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000107.ldb new file mode 100644 index 0000000000..33c48c0b3e Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/000107.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT index 00f4669871..abdfdfe276 100644 --- a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT @@ -1 +1 @@ -MANIFEST-000091 +MANIFEST-000106 diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak index 948a0b647f..e333c89b48 100644 --- a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000088 +MANIFEST-000102 diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG index 1b30a4e429..f9e576d734 100644 --- a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/LOG @@ -389,3 +389,70 @@ 17:59:01.708096 table@remove removed @73 17:59:27.006678 db@close closing 17:59:27.006727 db@close done T·48.84µs +=============== Oct 17, 2024 (UTC) =============== +05:10:40.941929 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.942055 version@stat F·[0 1] S·979KiB[0B 979KiB] Sc·[0.00 0.01] +05:10:40.942068 db@open opening +05:10:40.942101 journal@recovery F·1 +05:10:40.943919 journal@recovery recovering @90 +05:10:40.944706 memdb@flush created L0@93 N·30 S·3KiB "BH:..e6d,v6218":"blo..ore,v6215" +05:10:40.945097 version@stat F·[1 1] S·983KiB[3KiB 979KiB] Sc·[0.25 0.01] +05:10:40.947901 db@janitor F·5 G·1 +05:10:40.947908 db@janitor removing table-89 +05:10:40.947956 db@open done T·5.883755ms +05:11:11.986457 table@compaction L0·1 -> L1·1 S·983KiB Q·6276 +05:11:11.992423 table@build created L1@96 N·5181 S·982KiB "BH:..c5c,v363":"blo..ore,v6239" +05:11:11.992451 version@stat F·[0 1] S·982KiB[0B 982KiB] Sc·[0.00 0.01] +05:11:11.993034 table@compaction committed F-1 S-280B Ke·0 D·5 T·6.5542ms +05:11:11.993266 table@remove removed @92 +05:22:45.688799 db@close closing +05:22:45.688852 db@close done T·52.32µs +=============== Oct 17, 2024 (UTC) =============== +06:44:58.731477 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.731572 version@stat F·[0 1] S·982KiB[0B 982KiB] Sc·[0.00 0.01] +06:44:58.731582 db@open opening +06:44:58.731608 journal@recovery F·1 +06:44:58.733590 journal@recovery recovering @94 +06:44:58.741468 memdb@flush created L0@97 N·864 S·142KiB "BH:..91c,v6735":"blo..ore,v6246" +06:44:58.741742 version@stat F·[1 1] S·1MiB[142KiB 982KiB] Sc·[0.25 0.01] +06:44:58.757548 db@janitor F·5 G·1 +06:44:58.757554 db@janitor removing table-93 +06:44:58.757586 db@open done T·26.001494ms +06:45:36.822661 db@close closing +06:45:36.822719 db@close done T·57.27µs +=============== Oct 17, 2024 (UTC) =============== +06:45:46.666531 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.666665 version@stat F·[1 1] S·1MiB[142KiB 982KiB] Sc·[0.25 0.01] +06:45:46.666678 db@open opening +06:45:46.666711 journal@recovery F·1 +06:45:46.668662 journal@recovery recovering @98 +06:45:46.669474 memdb@flush created L0@100 N·42 S·10KiB "BH:..b99,v7144":"blo..ore,v7111" +06:45:46.669607 version@stat F·[2 1] S·1MiB[152KiB 982KiB] Sc·[0.50 0.01] +06:45:46.672153 db@janitor F·5 G·0 +06:45:46.672180 db@open done T·5.497737ms +06:46:16.625229 table@compaction L0·2 -> L1·1 S·1MiB Q·7178 +06:46:16.632031 table@build created L1@103 N·5936 S·1MiB "BH:..c5c,v363":"blo..ore,v7147" +06:46:16.632059 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +06:46:16.632639 table@compaction committed F-2 S+122B Ke·0 D·151 T·7.390783ms +06:46:16.632728 table@remove removed @97 +06:46:16.632918 table@remove removed @96 +06:47:06.119564 db@close closing +06:47:06.119632 db@close done T·68.141µs +=============== Oct 17, 2024 (UTC) =============== +06:47:26.289119 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.289213 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +06:47:26.289224 db@open opening +06:47:26.289255 journal@recovery F·1 +06:47:26.291626 journal@recovery recovering @101 +06:47:26.293004 memdb@flush created L0@104 N·90 S·14KiB "BH:..022,v7211":"blo..ore,v7154" +06:47:26.293131 version@stat F·[1 1] S·1MiB[14KiB 1MiB] Sc·[0.25 0.01] +06:47:26.295912 db@janitor F·5 G·1 +06:47:26.295920 db@janitor removing table-100 +06:47:26.295955 db@open done T·6.727058ms +06:47:51.340007 table@compaction L0·1 -> L1·1 S·1MiB Q·7263 +06:47:51.346786 table@build created L1@107 N·6011 S·1MiB "BH:..c5c,v363":"blo..ore,v7238" +06:47:51.346809 version@stat F·[0 1] S·1MiB[0B 1MiB] Sc·[0.00 0.01] +06:47:51.348343 table@compaction committed F-1 S-97B Ke·0 D·15 T·8.315581ms +06:47:51.348639 table@remove removed @103 +06:47:53.050412 db@close closing +06:47:53.050476 db@close done T·62.691µs diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 deleted file mode 100644 index 849bb8a758..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000091 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000106 b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000106 new file mode 100644 index 0000000000..3959a2ff27 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/blockstore.db/MANIFEST-000106 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal b/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal index 1e135caef0..a0de7456d1 100644 Binary files a/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal and b/.docker/container-state/nucleus-testnet-data/data/cs.wal/wal differ diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/000062.log b/.docker/container-state/nucleus-testnet-data/data/evidence.db/000070.log similarity index 100% rename from .docker/container-state/nucleus-testnet-data/data/evidence.db/000062.log rename to .docker/container-state/nucleus-testnet-data/data/evidence.db/000070.log diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT index e8c02667ae..be93edb695 100644 --- a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT @@ -1 +1 @@ -MANIFEST-000063 +MANIFEST-000071 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak index ebafc63b8e..5893b8f83b 100644 --- a/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000061 +MANIFEST-000069 diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG b/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG index bba04859e3..603afd6c50 100644 --- a/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/evidence.db/LOG @@ -283,3 +283,39 @@ 17:59:01.713504 version@stat F·[] S·0B[] Sc·[] 17:59:01.716861 db@janitor F·2 G·0 17:59:01.716876 db@open done T·3.62282ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.957492 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.957616 version@stat F·[] S·0B[] Sc·[] +05:10:40.957629 db@open opening +05:10:40.957658 journal@recovery F·1 +05:10:40.957894 journal@recovery recovering @62 +05:10:40.958388 version@stat F·[] S·0B[] Sc·[] +05:10:40.961122 db@janitor F·2 G·0 +05:10:40.961141 db@open done T·3.507417ms +=============== Oct 17, 2024 (UTC) =============== +06:44:58.803399 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.803504 version@stat F·[] S·0B[] Sc·[] +06:44:58.803519 db@open opening +06:44:58.803556 journal@recovery F·1 +06:44:58.803645 journal@recovery recovering @64 +06:44:58.803912 version@stat F·[] S·0B[] Sc·[] +06:44:58.813046 db@janitor F·2 G·0 +06:44:58.813055 db@open done T·9.533813ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.688140 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.688207 version@stat F·[] S·0B[] Sc·[] +06:45:46.688217 db@open opening +06:45:46.688244 journal@recovery F·1 +06:45:46.688329 journal@recovery recovering @66 +06:45:46.688475 version@stat F·[] S·0B[] Sc·[] +06:45:46.691028 db@janitor F·2 G·0 +06:45:46.691045 db@open done T·2.823914ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.306936 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.307069 version@stat F·[] S·0B[] Sc·[] +06:47:26.307085 db@open opening +06:47:26.307124 journal@recovery F·1 +06:47:26.307921 journal@recovery recovering @68 +06:47:26.308103 version@stat F·[] S·0B[] Sc·[] +06:47:26.310856 db@janitor F·2 G·0 +06:47:26.310866 db@open done T·3.776282ms diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 b/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 deleted file mode 100644 index f5b9b4efb8..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000063 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000071 b/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000071 new file mode 100644 index 0000000000..b4aa9a9181 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/evidence.db/MANIFEST-000071 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json b/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json index 8bf82338a8..70797c4328 100644 --- a/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json +++ b/.docker/container-state/nucleus-testnet-data/data/priv_validator_state.json @@ -1,7 +1,7 @@ { - "height": "1036", + "height": "1207", "round": 0, "step": 3, - "signature": "v27Rs6us+6Q7cj+aLxVHFzzu3VuuAnLMflQ/rhuAh4CgOngnQ5pvY25d3yFM32fBXneJEm0bRYy/30RDgYtEDA==", - "signbytes": "740802110C0400000000000022480A20893D8ECF312013CEA3102503BF9E39C5C78EB6AEAFA0DE202529F944D899793B122408011220D72D5FD098990DF6F3EC52E4602F660D611936C04D5DD307585531367D52F82C2A0C08FEA296B4061082BDE6E502320F6E75636C6575732D746573746E6574" + "signature": "icgswVCjm79XUsbei+n25O+rpUOvze+sTf2mK8ldJjFTDKocmcTEENgkYLa9qUoOTtAw8QgluaDJgzmiiBjwDg==", + "signbytes": "74080211B70400000000000022480A20284E4194EEBC89AFFBE87A98751F5A51DA90E72D8D6345FA1D2BBFEB89343EFC12240801122015262FE05A2AC0DA993FDF279A954CB68A15673F1521A48D39E7D65768595D802A0C0897E6C2B806108C99CBA301320F6E75636C6575732D746573746E6574" } \ No newline at end of file diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000062.log b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000070.log similarity index 100% rename from .docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000062.log rename to .docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/000070.log diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT index e8c02667ae..be93edb695 100644 --- a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT @@ -1 +1 @@ -MANIFEST-000063 +MANIFEST-000071 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak index ebafc63b8e..5893b8f83b 100644 --- a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000061 +MANIFEST-000069 diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG index f1dec582ce..1210bffa25 100644 --- a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/LOG @@ -283,3 +283,39 @@ 17:59:01.671281 version@stat F·[] S·0B[] Sc·[] 17:59:01.674069 db@janitor F·2 G·0 17:59:01.674079 db@open done T·3.076606ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.917433 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.917549 version@stat F·[] S·0B[] Sc·[] +05:10:40.917564 db@open opening +05:10:40.917602 journal@recovery F·1 +05:10:40.917711 journal@recovery recovering @62 +05:10:40.918030 version@stat F·[] S·0B[] Sc·[] +05:10:40.922003 db@janitor F·2 G·0 +05:10:40.922017 db@open done T·4.448515ms +=============== Oct 17, 2024 (UTC) =============== +06:44:58.694325 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.694411 version@stat F·[] S·0B[] Sc·[] +06:44:58.694425 db@open opening +06:44:58.694464 journal@recovery F·1 +06:44:58.694541 journal@recovery recovering @64 +06:44:58.694784 version@stat F·[] S·0B[] Sc·[] +06:44:58.713362 db@janitor F·2 G·0 +06:44:58.713379 db@open done T·18.950723ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.641800 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.641874 version@stat F·[] S·0B[] Sc·[] +06:45:46.641887 db@open opening +06:45:46.641917 journal@recovery F·1 +06:45:46.642008 journal@recovery recovering @66 +06:45:46.642134 version@stat F·[] S·0B[] Sc·[] +06:45:46.646348 db@janitor F·2 G·0 +06:45:46.646361 db@open done T·4.469118ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.263677 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.263742 version@stat F·[] S·0B[] Sc·[] +06:47:26.263752 db@open opening +06:47:26.263776 journal@recovery F·1 +06:47:26.263857 journal@recovery recovering @68 +06:47:26.264177 version@stat F·[] S·0B[] Sc·[] +06:47:26.269116 db@janitor F·2 G·0 +06:47:26.269129 db@open done T·5.373646ms diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 deleted file mode 100644 index f5b9b4efb8..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000063 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000071 b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000071 new file mode 100644 index 0000000000..b4aa9a9181 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/snapshots/metadata.db/MANIFEST-000071 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb b/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb deleted file mode 100644 index ce41e60f12..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/state.db/000091.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000106.ldb b/.docker/container-state/nucleus-testnet-data/data/state.db/000106.ldb new file mode 100644 index 0000000000..41709b5d5a Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/000106.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000092.log b/.docker/container-state/nucleus-testnet-data/data/state.db/000107.log similarity index 83% rename from .docker/container-state/nucleus-testnet-data/data/state.db/000092.log rename to .docker/container-state/nucleus-testnet-data/data/state.db/000107.log index 7aa8691b72..57c76a9bb0 100644 Binary files a/.docker/container-state/nucleus-testnet-data/data/state.db/000092.log and b/.docker/container-state/nucleus-testnet-data/data/state.db/000107.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb b/.docker/container-state/nucleus-testnet-data/data/state.db/000109.ldb similarity index 70% rename from .docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb rename to .docker/container-state/nucleus-testnet-data/data/state.db/000109.ldb index 72caa9fb6f..c3d631b9d3 100644 Binary files a/.docker/container-state/nucleus-testnet-data/data/state.db/000088.ldb and b/.docker/container-state/nucleus-testnet-data/data/state.db/000109.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT index f60e23b00c..db6fa61f96 100644 --- a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT @@ -1 +1 @@ -MANIFEST-000093 +MANIFEST-000108 diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak index 2f2c868af7..c8e9be65ea 100644 --- a/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000090 +MANIFEST-000104 diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/LOG b/.docker/container-state/nucleus-testnet-data/data/state.db/LOG index c811eadd21..ceaaced552 100644 --- a/.docker/container-state/nucleus-testnet-data/data/state.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/state.db/LOG @@ -394,3 +394,75 @@ 17:59:01.705289 db@open done T·5.934209ms 17:59:27.006738 db@close closing 17:59:27.006773 db@close done T·35.621µs +=============== Oct 17, 2024 (UTC) =============== +05:10:40.948045 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.948116 version@stat F·[1 1] S·480KiB[87KiB 392KiB] Sc·[0.25 0.00] +05:10:40.948126 db@open opening +05:10:40.948156 journal@recovery F·1 +05:10:40.948359 journal@recovery recovering @92 +05:10:40.949167 memdb@flush created L0@94 N·25 S·5KiB "abc..032,v5184":"val..038,v5206" +05:10:40.949303 version@stat F·[2 1] S·485KiB[93KiB 392KiB] Sc·[0.50 0.00] +05:10:40.952715 db@janitor F·5 G·0 +05:10:40.952726 db@open done T·4.596875ms +05:10:52.975901 table@compaction L0·2 -> L1·1 S·485KiB Q·5219 +05:10:52.980319 table@build created L1@97 N·3114 S·432KiB "abc..y:1,v6":"val..999,v5010" +05:10:52.980342 version@stat F·[0 1] S·432KiB[0B 432KiB] Sc·[0.00 0.00] +05:10:52.980914 table@compaction committed F-2 S-53KiB Ke·0 D·148 T·4.995718ms +05:10:52.980988 table@remove removed @91 +05:10:52.981079 table@remove removed @88 +05:22:45.688861 db@close closing +05:22:45.688894 db@close done T·32.78µs +=============== Oct 17, 2024 (UTC) =============== +06:44:58.757687 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.757762 version@stat F·[0 1] S·432KiB[0B 432KiB] Sc·[0.00 0.00] +06:44:58.757772 db@open opening +06:44:58.757803 journal@recovery F·1 +06:44:58.758114 journal@recovery recovering @95 +06:44:58.766158 memdb@flush created L0@98 N·720 S·160KiB "abc..037,v5210":"val..182,v5927" +06:44:58.768181 version@stat F·[1 1] S·593KiB[160KiB 432KiB] Sc·[0.25 0.00] +06:44:58.782781 db@janitor F·5 G·1 +06:44:58.782787 db@janitor removing table-94 +06:44:58.782815 db@open done T·25.040516ms +06:45:31.737199 table@compaction L0·1 -> L1·1 S·593KiB Q·5960 +06:45:31.742113 table@build created L1@101 N·3546 S·502KiB "abc..y:1,v6":"val..999,v5010" +06:45:31.742138 version@stat F·[0 1] S·502KiB[0B 502KiB] Sc·[0.00 0.00] +06:45:31.742713 table@compaction committed F-1 S-90KiB Ke·0 D·288 T·5.489518ms +06:45:31.742852 table@remove removed @97 +06:45:36.822729 db@close closing +06:45:36.822759 db@close done T·30.251µs +=============== Oct 17, 2024 (UTC) =============== +06:45:46.672283 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.672368 version@stat F·[0 1] S·502KiB[0B 502KiB] Sc·[0.00 0.00] +06:45:46.672378 db@open opening +06:45:46.672405 journal@recovery F·1 +06:45:46.672600 journal@recovery recovering @99 +06:45:46.673679 memdb@flush created L0@102 N·35 S·14KiB "abc..181,v5931":"val..189,v5963" +06:45:46.674067 version@stat F·[1 1] S·517KiB[14KiB 502KiB] Sc·[0.25 0.00] +06:45:46.676665 db@janitor F·5 G·1 +06:45:46.676674 db@janitor removing table-98 +06:45:46.676808 db@open done T·4.425948ms +06:46:17.600950 table@compaction L0·1 -> L1·1 S·517KiB Q·5996 +06:46:17.606418 table@build created L1@105 N·3567 S·509KiB "abc..y:1,v6":"val..999,v5010" +06:46:17.606444 version@stat F·[0 1] S·509KiB[0B 509KiB] Sc·[0.00 0.00] +06:46:17.607473 table@compaction committed F-1 S-8KiB Ke·0 D·14 T·6.501365ms +06:46:17.607615 table@remove removed @101 +06:47:06.119645 db@close closing +06:47:06.119690 db@close done T·44.54µs +=============== Oct 17, 2024 (UTC) =============== +06:47:26.296053 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.296123 version@stat F·[0 1] S·509KiB[0B 509KiB] Sc·[0.00 0.00] +06:47:26.296133 db@open opening +06:47:26.296160 journal@recovery F·1 +06:47:26.296240 journal@recovery recovering @103 +06:47:26.297208 memdb@flush created L0@106 N·75 S·14KiB "abc..188,v5967":"val..204,v6039" +06:47:26.297326 version@stat F·[1 1] S·523KiB[14KiB 509KiB] Sc·[0.25 0.00] +06:47:26.300025 db@janitor F·5 G·1 +06:47:26.300032 db@janitor removing table-102 +06:47:26.300068 db@open done T·3.931804ms +06:47:51.340428 table@compaction L0·1 -> L1·1 S·523KiB Q·6062 +06:47:51.345463 table@build created L1@109 N·3612 S·515KiB "abc..y:1,v6":"val..999,v5010" +06:47:51.345490 version@stat F·[0 1] S·515KiB[0B 515KiB] Sc·[0.00 0.01] +06:47:51.347253 table@compaction committed F-1 S-8KiB Ke·0 D·30 T·6.790988ms +06:47:51.347409 table@remove removed @105 +06:47:53.050487 db@close closing +06:47:53.050515 db@close done T·27.49µs diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 b/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 deleted file mode 100644 index f044b3f1c5..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000093 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000108 b/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000108 new file mode 100644 index 0000000000..3e0e6991cd Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/state.db/MANIFEST-000108 differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb deleted file mode 100644 index 3c915bd371..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000078.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb deleted file mode 100644 index d228e9d6d5..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000079.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb deleted file mode 100644 index dbe9644aa0..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000084.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb deleted file mode 100644 index c1388b6fb7..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000089.ldb and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log deleted file mode 100644 index 0897ff3654..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000090.log and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000095.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000095.ldb new file mode 100644 index 0000000000..d60494d2f4 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000095.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000096.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000096.ldb new file mode 100644 index 0000000000..ced510724b Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000096.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000099.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000099.ldb new file mode 100644 index 0000000000..835a70959c Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000099.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000102.ldb b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000102.ldb new file mode 100644 index 0000000000..b1a9417bb5 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000102.ldb differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000103.log b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000103.log new file mode 100644 index 0000000000..83c9380d98 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/000103.log differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT index 00f4669871..c8e9be65ea 100644 --- a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT @@ -1 +1 @@ -MANIFEST-000091 +MANIFEST-000104 diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak index 948a0b647f..ed5ac889a6 100644 --- a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/CURRENT.bak @@ -1 +1 @@ -MANIFEST-000088 +MANIFEST-000101 diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG index 6f949c5d7c..d472949dbb 100644 --- a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG +++ b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/LOG @@ -350,3 +350,52 @@ 17:59:01.708419 version@stat F·[3 1] S·550KiB[60KiB 490KiB] Sc·[0.75 0.00] 17:59:01.712571 db@janitor F·6 G·0 17:59:01.712584 db@open done T·6.619955ms +=============== Oct 17, 2024 (UTC) =============== +05:10:40.953178 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +05:10:40.953252 version@stat F·[3 1] S·550KiB[60KiB 490KiB] Sc·[0.75 0.00] +05:10:40.953263 db@open opening +05:10:40.953304 journal@recovery F·1 +05:10:40.953387 journal@recovery recovering @90 +05:10:40.954217 memdb@flush created L0@92 N·175 S·2KiB "blo..\x01\xc4\b,v36695":"blo..k\x00\x01,v36845" +05:10:40.954329 version@stat F·[4 1] S·553KiB[63KiB 490KiB] Sc·[1.00 0.00] +05:10:40.956867 db@janitor F·7 G·0 +05:10:40.956878 db@open done T·3.611318ms +05:10:40.956914 table@compaction L0·4 -> L1·1 S·553KiB Q·36870 +05:10:40.970187 table@build created L1@95 N·30671 S·553KiB "\tr\xf8..pSV,v26111":"\xf2~\xe4..\x05\xa4\xc4,v21827" +05:10:40.970217 version@stat F·[0 1] S·553KiB[0B 553KiB] Sc·[0.00 0.01] +05:10:40.970807 table@compaction committed F-4 S-550B Ke·0 D·447 T·13.871697ms +05:10:40.970885 table@remove removed @89 +05:10:40.970918 table@remove removed @84 +05:10:40.970943 table@remove removed @79 +05:10:40.971049 table@remove removed @78 +=============== Oct 17, 2024 (UTC) =============== +06:44:58.783278 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:44:58.783368 version@stat F·[0 1] S·553KiB[0B 553KiB] Sc·[0.00 0.01] +06:44:58.783380 db@open opening +06:44:58.783411 journal@recovery F·1 +06:44:58.783501 journal@recovery recovering @93 +06:44:58.792589 memdb@flush created L0@96 N·5227 S·104KiB "\t\xf8|..\"=\xa9,v39810":"\x95\xbat..\xc7FU,v40268" +06:44:58.792706 version@stat F·[1 1] S·657KiB[104KiB 553KiB] Sc·[0.25 0.01] +06:44:58.802828 db@janitor F·5 G·1 +06:44:58.802834 db@janitor removing table-92 +06:44:58.802858 db@open done T·19.474428ms +=============== Oct 17, 2024 (UTC) =============== +06:45:46.677216 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:45:46.677292 version@stat F·[1 1] S·657KiB[104KiB 553KiB] Sc·[0.25 0.01] +06:45:46.677310 db@open opening +06:45:46.677340 journal@recovery F·1 +06:45:46.677425 journal@recovery recovering @97 +06:45:46.678375 memdb@flush created L0@99 N·302 S·14KiB "\\\x7f\xf1..AP\b,v42182":"\xbf\x87\xea..]\x96\xf3,v42260" +06:45:46.678538 version@stat F·[2 1] S·671KiB[118KiB 553KiB] Sc·[0.50 0.01] +06:45:46.687729 db@janitor F·5 G·0 +06:45:46.687742 db@open done T·10.428559ms +=============== Oct 17, 2024 (UTC) =============== +06:47:26.300462 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:47:26.300551 version@stat F·[2 1] S·671KiB[118KiB 553KiB] Sc·[0.50 0.01] +06:47:26.300565 db@open opening +06:47:26.300606 journal@recovery F·1 +06:47:26.300688 journal@recovery recovering @100 +06:47:26.301782 memdb@flush created L0@102 N·525 S·7KiB "blo..\x01Ĥ,v42402":"blo..k\x00\x01,v42902" +06:47:26.301919 version@stat F·[3 1] S·678KiB[125KiB 553KiB] Sc·[0.75 0.01] +06:47:26.306059 db@janitor F·6 G·0 +06:47:26.306075 db@open done T·5.504547ms diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 deleted file mode 100644 index d75cffd4ec..0000000000 Binary files a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000091 and /dev/null differ diff --git a/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000104 b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000104 new file mode 100644 index 0000000000..57f27cbda8 Binary files /dev/null and b/.docker/container-state/nucleus-testnet-data/data/tx_index.db/MANIFEST-000104 differ diff --git a/.github/actions/build-cache/action.yml b/.github/actions/build-cache/action.yml new file mode 100644 index 0000000000..c5af862e1b --- /dev/null +++ b/.github/actions/build-cache/action.yml @@ -0,0 +1,10 @@ +name: 'Set up build cache' +description: 'Sets up caching for KDF builds' +runs: + using: 'composite' + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up rust-cache + uses: Swatinem/rust-cache@v2 diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml deleted file mode 100644 index 89069e7797..0000000000 --- a/.github/actions/cargo-cache/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Set up cargo cache' -description: 'Sets up the cargo cache for the workflow' -runs: - using: 'composite' - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up cargo cache - uses: actions/cache@v3 - continue-on-error: false - with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index a4fce83cbe..8877bb518f 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -46,8 +46,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -119,8 +119,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -180,8 +180,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -240,8 +240,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -303,8 +303,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -368,7 +368,8 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -378,8 +379,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -430,8 +431,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -505,8 +506,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -582,8 +583,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | diff --git a/.github/workflows/fmt-and-lint.yml b/.github/workflows/fmt-and-lint.yml index f5ea217eee..b5212320dd 100644 --- a/.github/workflows/fmt-and-lint.yml +++ b/.github/workflows/fmt-and-lint.yml @@ -26,8 +26,8 @@ jobs: with: deps: ('protoc' 'libudev-dev') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: fmt check # Format checks aren't OS dependant. @@ -54,8 +54,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: clippy lint run: cargo clippy --target wasm32-unknown-unknown -- --D warnings diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 59e176a5e0..6ce7dedd0a 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -22,6 +22,7 @@ jobs: types: | feat fix + improvement chore docs deps @@ -38,16 +39,18 @@ jobs: TITLE: ${{ github.event.pull_request.title }} run: | title_length=${#TITLE} - if [ $title_length -gt 72 ] + if [ $title_length -gt 85 ] then - echo "PR title is too long (greater than 72 characters)" + echo "PR title is too long (greater than 85 characters)" exit 1 fi - name: Check PR labels env: LABEL_NAMES: ${{ toJson(github.event.pull_request.labels.*.name) }} - if: contains(env.LABEL_NAMES, 'under review') == contains(env.LABEL_NAMES, 'in progress') + if: "!((contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && contains(env.LABEL_NAMES, 'in progress') && !contains(env.LABEL_NAMES, 'blocked')) + || (!contains(env.LABEL_NAMES, 'pending review') && !contains(env.LABEL_NAMES, 'in progress') && contains(env.LABEL_NAMES, 'blocked')))" run: | - echo "PR must have "exactly one" of these labels: ['under review', 'in progress']." + echo "PR must have "exactly one" of these labels: ['status: pending review', 'status: in progress', 'status: blocked']." exit 1 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index a74a589d10..cae080b3d9 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -46,8 +46,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -110,8 +110,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -165,8 +165,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -219,8 +219,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $Env:GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -275,8 +275,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -334,7 +334,8 @@ jobs: rustup target add wasm32-unknown-unknown - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | bash -s -- -y + # TODO: Lock wasm-pack version + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Calculate commit hash for PR commit if: github.event_name == 'pull_request' @@ -344,8 +345,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -393,8 +394,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -462,8 +463,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | @@ -533,8 +534,8 @@ jobs: if: github.event_name != 'pull_request' run: echo "COMMIT_HASH=$(git rev-parse --short=7 HEAD)" >> $GITHUB_ENV - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Build run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 12a60bbc3c..53debcb4b2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,8 +33,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -61,8 +61,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -89,8 +89,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -117,8 +117,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -149,8 +149,8 @@ jobs: - name: Set loopback address run: ./scripts/ci/lo0_config.sh - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -178,8 +178,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Download wget64 uses: ./.github/actions/download-and-verify @@ -214,8 +214,8 @@ jobs: with: deps: ('protoc') - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: | @@ -245,6 +245,9 @@ jobs: deps: ('protoc') - name: Install wasm-pack + # Use the latest wasm-pack for up-to-date compatibility coverage on KDF. + # As we don't share any build artifacts from this pipeline, we don't need + # to lock the version here. run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Download geckodriver @@ -261,8 +264,8 @@ jobs: sudo tar -xzvf geckodriver-v0.32.2-linux64.tar.gz -C /bin sudo chmod +x /bin/geckodriver - - name: Cargo cache - uses: ./.github/actions/cargo-cache + - name: Build cache + uses: ./.github/actions/build-cache - name: Test run: WASM_BINDGEN_TEST_TIMEOUT=480 GECKODRIVER=/bin/geckodriver wasm-pack test --firefox --headless mm2src/mm2_main diff --git a/.github/workflows/validate-cargo-lock.yml b/.github/workflows/validate-cargo-lock.yml new file mode 100644 index 0000000000..eb59173d28 --- /dev/null +++ b/.github/workflows/validate-cargo-lock.yml @@ -0,0 +1,16 @@ +name: Validate Cargo.lock +on: [push] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + validate-cargo-lock: + name: Checking Cargo.lock file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Validate Cargo.lock + run: cargo update -w --locked diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a2424397..f3d8816316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +## v2.2.0-beta - 2024-11-22 + +**Features:** +- Connection Healthcheck + - Connection healthcheck implementation for peers was introduced. [#2194](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2194) +- Custom Tokens Activation + - Support for enabling custom EVM (ERC20, PLG20, etc..) tokens without requiring them to be in the coins config was added. [#2141](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2141) + - This allows users to interact with any ERC20 token by providing the contract address. + +**Enhancements/Fixes:** +- Trading Protocol Upgrade [#1895](https://github.com/KomodoPlatform/atomicDEX-API/issues/1895) + - EVM TPU taker methods were implemented and enhancements were made to ETH docker tests. [#2169](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2169) + - EVM TPU maker methods were implemented. [#2211](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2211) +- NFT integration [#900](https://github.com/KomodoPlatform/atomicDEX-API/issues/900) + - Refund methods for NFT swaps were completed. [#2129](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2129) + - `token_id` field was added to the tx history primary key. [#2209](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2209) +- Graceful Shutdown + - CTRL-C signal handling with graceful shutdown was implemented. [#2213](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2213) +- Seed Management [#1939](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1939) + - A new `get_wallet_names` RPC was added to retrieve information about all wallet names and the currently active one. [#2202](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2202) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Cosmos tx broadcasting error was fixed by upgrading cosmrs to version 15. [#2238](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2238) + - Cosmos transaction history implementation was incorrectly parsing addresses (using the relayer address instead of the cross-chain address) from IBC transactions. The address parsing logic was fixed in [#2245](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2245) +- Order Management + - Cancel order race condition was addressed using time-based cache. [#2232](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2232) +- Swap Improvements + - A legacy swap issue was resolved where taker spent maker payment transactions were sometimes incorrectly marked as successful when they were actually reverted or not confirmed, particularly in EVM-based swaps. [#2199](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2199) + - Two new events were added: "MakerPaymentSpendConfirmed" and "MakerPaymentSpendConfirmFailed" + - A fix was introduced where Takers don't need to confirm their own payment as they can wait for the spending of it straight away. [#2249](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2249) + - This invalidates this fix [#1442](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1442), a better solution will be introduced where taker rebroadcasts their transaction if it's not on the chain. + - A fix was introduced for recover funds for takers when the swap was marked as unsuccessful due to the maker payment spend transaction not being confirmed. [#2242](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2242) + - The required confirmations from coin config for taker/maker payment spend are now used instead of using 1 confirmation max. This is because some chains require more than 1 confirmation for finality, e.g. Polygon. +- Swap watchers [#1431](https://github.com/KomodoPlatform/atomicDEX-API/issues/1431) + - Taker fee validation retries now work the same way as for makers. [#2263](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2263) +- Electrum Client + - Electrum client was refactored to add min/max connection controls, with server priority based on list order. [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - Electrum client can now operate in single-server mode (1,1) to reduce resource usage (especially beneficial for mobile) or multi-server (legacy) mode for reliability. + - Higher priority servers automatically replace lower priority ones when reconnecting during periodic retries or when connection count drops below minimum. +- Coins Activation + - EVM addresses are now displayed in full in iguana v2 activation response. [#2254](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2254) +- HD Wallet [#1838](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1838) + - Balance is now returned as `CoinBalanceMap` for both UTXOs and QTUM. [#2259](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2259) + - This is to return the same type/json across all coins for GUIs since EVM uses `CoinBalanceMap`. + - EVM addresses are displayed in full in `get_new_address` response after [#2264](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2264) +- RPC Service + - A fix was introduced to run rpc request futures till completion in [#1966](https://github.com/KomodoPlatform/komodo-defi-framework/pull/1966) + - This ensures RPC request futures complete fully even if clients disconnect, preventing partial state updates and maintaining data consistency. +- Security Enhancements + - Message lifetime overflows were added to prevent creating messages for proxy with too long lifetimes. [#2233](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2233) + - Remote files are now handled in a safer way in CI. [#2217](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2217) +- Build Process + - `wasm-opt` overriding was removed. [#2200](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2200) +- Escaped response body in native RPC was removed. [#2219](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2219) +- Creation of the all-zeroes dir on KDF start was stopped. [#2218](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2218) +- OPTIONS requests to KDF server were added. [#2191](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2191) + +**Removals:** +- Solana Support [#1085](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1085) + - Solana implementation was removed until it can be redone using the latest Solana SDK. [#2239](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2239) +- Adex-CLI [#1682](https://github.com/KomodoPlatform/atomicDEX-API/issues/1682) + - adex-cli was deprecated pending work on a simpler, more maintainable implementation. [#2234](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2234) + +**Other Changes:** +- Documentation + - Issue link in README was updated. [#2227](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2227) + - Commit badges were updated to use dev branch in README. [#2193](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2193) + - Leftover subcommands were removed from help message. [#2235](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2235) [#2270](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2270) +- Code Structure + - lib.rs was replaced by mm2.rs as the root lib for mm2_main. [#2178](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2178) +- Code Improvements + - P2P feature was added to mm2_net dependency to allow the coins crate to be compiled and tested independently. [#2210](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2210) + - Coins mod clippy warnings in WASM were fixed. [#2224](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2224) + - Nonsense CLI arguments were removed. [#2216](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2216) +- Tests + - Tendermint IBC tests were fixed by preparing IBC channels inside the container. [#2246](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2246) + - `.wait()` usage was replaced with `block_on` in tests to ensure consistent runtime usage, fixing issues with tokio TCP streams in non-tokio runtimes. [#2220](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2220) + - Debug assertions for tests were enabled. [#2204](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2204) + - More Sepolia test endpoints were added in [#2262](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2262) + +**NB - Backwards compatibility breaking changes:** +- RPC Renaming + - `get_peers_info` RPC was renamed to `get_directly_connected_peers`. [#2195](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2195) +- Cosmos Integration [#1432](https://github.com/KomodoPlatform/atomicDEX-API/issues/1432) + - Updates to Tendermint activation payloads: + - 'rpc_urls' field (previously a list of plain string values) is replaced with 'nodes' (a list of JSON objects). [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) +- Komodo DeFi Proxy + - All RPC methods fields controlling komodo-defi-proxy are renamed to 'komodo_proxy', affecting various activations, including ETH/EVM. [#2173](https://github.com/KomodoPlatform/komodo-defi-framework/pull/2173) + + ## v2.1.0-beta - 2024-07-31 **Features:** diff --git a/Cargo.lock b/Cargo.lock index f0cef862ae..1e0790f40f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,6 +1013,7 @@ dependencies = [ "log", "parking_lot", "parking_lot_core 0.6.2", + "paste", "primitive-types", "rand 0.7.3", "regex", @@ -1102,9 +1103,9 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +checksum = "82e23f6ab56d5f031cde05b8b82a5fefd3a1a223595c79e32317a97189e612bc" dependencies = [ "prost", "prost-types", @@ -1113,9 +1114,9 @@ dependencies = [ [[package]] name = "cosmrs" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +checksum = "5d184abb7b0039cc64f282dfa5b34165e4c5a7410ab46804636d53f4d09aee44" dependencies = [ "cosmos-sdk-proto", "ecdsa", @@ -2055,7 +2056,6 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" dependencies = [ - "eyre", "paste", ] @@ -3100,7 +3100,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libp2p" version = "0.52.1" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "libp2p-allow-block-list" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3143,7 +3143,7 @@ dependencies = [ [[package]] name = "libp2p-connection-limits" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "libp2p-core", "libp2p-identity", @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "libp2p-core" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "fnv", @@ -3181,7 +3181,7 @@ dependencies = [ [[package]] name = "libp2p-dns" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "libp2p-core", @@ -3195,7 +3195,7 @@ dependencies = [ [[package]] name = "libp2p-floodsub" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "cuckoofilter", @@ -3215,7 +3215,7 @@ dependencies = [ [[package]] name = "libp2p-gossipsub" version = "0.45.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "base64 0.21.7", @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "libp2p-identify" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "either", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "libp2p-mdns" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "data-encoding", "futures 0.3.28", @@ -3306,7 +3306,7 @@ dependencies = [ [[package]] name = "libp2p-metrics" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "instant", "libp2p-core", @@ -3322,7 +3322,7 @@ dependencies = [ [[package]] name = "libp2p-noise" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "curve25519-dalek 3.2.0", @@ -3346,7 +3346,7 @@ dependencies = [ [[package]] name = "libp2p-ping" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -3363,7 +3363,7 @@ dependencies = [ [[package]] name = "libp2p-request-response" version = "0.25.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "async-trait", "futures 0.3.28", @@ -3380,7 +3380,7 @@ dependencies = [ [[package]] name = "libp2p-swarm" version = "0.43.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "fnv", @@ -3402,7 +3402,7 @@ dependencies = [ [[package]] name = "libp2p-swarm-derive" version = "0.33.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "heck", "proc-macro-warning", @@ -3414,7 +3414,7 @@ dependencies = [ [[package]] name = "libp2p-tcp" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "futures-timer", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "libp2p-wasm-ext" version = "0.40.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "js-sys", @@ -3443,7 +3443,7 @@ dependencies = [ [[package]] name = "libp2p-websocket" version = "0.42.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -3462,7 +3462,7 @@ dependencies = [ [[package]] name = "libp2p-yamux" version = "0.44.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "either", "futures 0.3.28", @@ -3841,7 +3841,7 @@ dependencies = [ [[package]] name = "mm2_bin_lib" -version = "2.2.0-beta" +version = "2.3.0-beta" dependencies = [ "chrono", "common", @@ -4077,6 +4077,7 @@ dependencies = [ "num-traits", "parity-util-mem", "parking_lot", + "primitive-types", "primitives", "prost", "prost-build", @@ -4106,6 +4107,7 @@ dependencies = [ "spv_validation", "testcontainers", "tokio", + "trading_api", "trie-db", "trie-root 0.16.0", "url", @@ -4248,6 +4250,7 @@ dependencies = [ "sha2 0.10.7", "smallvec 1.6.1", "syn 2.0.38", + "timed-map", "tokio", "void", ] @@ -4378,7 +4381,7 @@ checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] name = "multistream-select" version = "0.13.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "bytes 1.4.0", "futures 0.3.28", @@ -4507,13 +4510,13 @@ 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", "quote 1.0.33", - "syn 1.0.95", + "syn 2.0.38", ] [[package]] @@ -4940,7 +4943,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", - "toml", + "toml 0.5.7", ] [[package]] @@ -5110,7 +5113,7 @@ dependencies = [ [[package]] name = "quick-protobuf-codec" version = "0.2.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "asynchronous-codec", "bytes 1.4.0", @@ -5711,9 +5714,9 @@ checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc-hex" @@ -5850,7 +5853,7 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rw-stream-sink" version = "0.4.0" -source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.4#6fc061b58853c1b0dafaa19a4a29343c0ac6eab3" +source = "git+https://github.com/KomodoPlatform/rust-libp2p.git?tag=k-0.52.11#f949f65669f5dbfb95eb38e7e9393b48202c2995" dependencies = [ "futures 0.3.28", "pin-project", @@ -6144,6 +6147,15 @@ dependencies = [ "syn 1.0.95", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6763,9 +6775,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" +checksum = "43f8a10105d0a7c4af0a242e23ed5a12519afe5cc0e68419da441bb5981a6802" dependencies = [ "bytes 1.4.0", "digest 0.10.7", @@ -6794,23 +6806,23 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +checksum = "ac6bf36c613bb113737c333e3c1d6dfd3c99f8ac679e84feb58dd6456d77fb2e" dependencies = [ "flex-error", "serde", "serde_json", "tendermint", - "toml", + "toml 0.8.19", "url", ] [[package]] name = "tendermint-proto" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" +checksum = "ff525d5540a9fc535c38dc0d92a98da3ee36fcdfbda99cecb9f3cce5cd4d41d7" dependencies = [ "bytes 1.4.0", "flex-error", @@ -6826,9 +6838,9 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" +checksum = "2d8fe61b1772cd50038bdeeadf53773bb37a09e639dd8e6d996668fd220ddb29" dependencies = [ "async-trait", "bytes 1.4.0", @@ -6944,6 +6956,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "timed-map" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b102d4d896895d697f1dff4141dff28307532dac57a376b2b5665a55b280dc6" +dependencies = [ + "rustc-hash", + "web-time", +] + [[package]] name = "tiny-keccak" version = "1.4.4" @@ -7114,6 +7136,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.2.3", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" @@ -7223,6 +7279,26 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trading_api" +version = "0.1.0" +dependencies = [ + "common", + "derive_more", + "enum_derives", + "ethereum-types", + "lazy_static", + "mm2_core", + "mm2_err_handle", + "mm2_net", + "mm2_number", + "mocktopus", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "trezor" version = "0.1.1" @@ -7709,6 +7785,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web3" version = "0.19.0" @@ -8056,6 +8142,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index ab18c83da1..507c2e5c31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ members = [ "mm2src/proxy_signature", "mm2src/rpc_task", "mm2src/trezor", + "mm2src/trading_api", ] exclude = [ diff --git a/docs/WASM_BUILD.md b/docs/WASM_BUILD.md index eb62fa7731..28355bbf80 100644 --- a/docs/WASM_BUILD.md +++ b/docs/WASM_BUILD.md @@ -1,12 +1,27 @@ # Building WASM binary +## From Container: + +If you want to build from source without installing prerequisites to your host system, you can do so by binding the source code inside a container and compiling it there. + +Build the image: + +```sh +docker build -t kdf-build-container -f .docker/Dockerfile . +``` + +Bind source code into container and compile it: +```sh +docker run -v "$(pwd)":/app -w /app kdf-build-container wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ +``` + ## Setting up the environment To build WASM binary from source, the following prerequisites are required: 1. Install `wasm-pack` ``` - cargo install wasm-pack + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh ``` 2. OSX specific: install `llvm` ``` @@ -39,4 +54,6 @@ If you want to disable optimizations to reduce the compilation time, run `wasm-p wasm-pack build mm2src/mm2_bin_lib --target web --out-dir wasm_build/deps/pkg/ --dev ``` -Please don't forget to specify `CC` and `AR` if you run the command on OSX. \ No newline at end of file +Please don't forget to specify `CC` and `AR` if you run the command on OSX. + + diff --git a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs index d58b035f73..1fef03bf30 100644 --- a/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs +++ b/mm2src/adex_cli/src/scenarios/init_mm2_cfg.rs @@ -107,7 +107,7 @@ impl Mm2Cfg { self.dbdir = CustomType::>::new("What is dbdir") .with_placeholder(DEFAULT_OPTION_PLACEHOLDER) - .with_help_message("AtomicDEX API database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") + .with_help_message("Komodo DeFi Framework database path. Optional, defaults to a subfolder named DB in the path of your mm2 binary") .with_validator(is_reachable_dir) .prompt() .map_err(|error| @@ -128,7 +128,7 @@ impl Mm2Cfg { fn inquire_net_id(&mut self) -> Result<()> { self.netid = CustomType::::new("What is the network `mm2` is going to be a part, netid:") .with_default(DEFAULT_NET_ID) - .with_help_message(r#"Network ID number, telling the AtomicDEX API which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) + .with_help_message(r#"Network ID number, telling the Komodo DeFi Framework which network to join. 8762 is the current main network, though alternative netids can be used for testing or "private" trades"#) .with_placeholder(format!("{DEFAULT_NET_ID}").as_str()) .prompt() .map_err(|error| @@ -268,7 +268,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("If false the AtomicDEX API will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") + .with_help_message("If false the Komodo DeFi Framework will allow rpc methods sent from external IP addresses. Optional, defaults to true. Warning: Only use this if you know what you are doing, and have put the appropriate security measures in place.") .prompt() .map_err(|error| error_anyhow!("Failed to get rpc_local_only: {error}") @@ -283,7 +283,7 @@ impl Mm2Cfg { .with_formatter(DEFAULT_OPTION_BOOL_FORMATTER) .with_default_value_formatter(DEFAULT_DEFAULT_OPTION_BOOL_FORMATTER) .with_default(InquireOption::None) - .with_help_message("Runs AtomicDEX API as a seed node mode (acting as a relay for AtomicDEX API clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other AtomicDEX API clients using the same netID.") + .with_help_message("Runs Komodo DeFi Framework as a seed node mode (acting as a relay for Komodo DeFi Framework clients). Optional, defaults to false. Use of this mode is not reccomended on the main network (8762) as it could result in a pubkey ban if non-compliant. on alternative testing or private networks, at least one seed node is required to relay information to other Komodo DeFi Framework clients using the same netID.") .prompt() .map_err(|error| error_anyhow!("Failed to get i_am_a_seed: {error}") diff --git a/mm2src/coins/Cargo.toml b/mm2src/coins/Cargo.toml index 06f250fd49..1280025a49 100644 --- a/mm2src/coins/Cargo.toml +++ b/mm2src/coins/Cargo.toml @@ -33,7 +33,7 @@ cfg-if = "1.0" chain = { path = "../mm2_bitcoin/chain" } chrono = { version = "0.4.23", "features" = ["serde"] } common = { path = "../common" } -cosmrs = { version = "0.15", default-features = false } +cosmrs = { version = "0.16", default-features = false } crossbeam = "0.8" crypto = { path = "../crypto" } db_common = { path = "../db_common" } @@ -69,7 +69,7 @@ mm2_io = { path = "../mm2_io" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number"} -mm2_p2p = { path = "../mm2_p2p" } +mm2_p2p = { path = "../mm2_p2p", default-features = false } mm2_rpc = { path = "../mm2_rpc" } mm2_state_machine = { path = "../mm2_state_machine" } mocktopus = { version = "0.8.0", optional = true } @@ -102,7 +102,7 @@ sha2 = "0.10" sha3 = "0.9" utxo_signer = { path = "utxo_signer" } # using the same version as cosmrs -tendermint-rpc = { version = "0.34", default-features = false } +tendermint-rpc = { version = "0.35", default-features = false } tokio-tungstenite-wasm = { git = "https://github.com/KomodoPlatform/tokio-tungstenite-wasm", rev = "d20abdb", features = ["rustls-tls-native-roots"]} url = { version = "2.2.2", features = ["serde"] } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } @@ -161,6 +161,7 @@ winapi = "0.3" [dev-dependencies] mm2_test_helpers = { path = "../mm2_test_helpers" } mocktopus = { version = "0.8.0" } +mm2_p2p = { path = "../mm2_p2p", features = ["application"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wagyu-zcash-parameters = { version = "0.2" } diff --git a/mm2src/coins/coin_errors.rs b/mm2src/coins/coin_errors.rs index aead751eb0..6075317bfc 100644 --- a/mm2src/coins/coin_errors.rs +++ b/mm2src/coins/coin_errors.rs @@ -1,12 +1,12 @@ -use crate::eth::eth_swap_v2::{PaymentStatusErr, ValidatePaymentV2Err}; -use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError, PrepareTxDataError}; +use crate::eth::eth_swap_v2::{PaymentStatusErr, PrepareTxDataError, ValidatePaymentV2Err}; +use crate::eth::nft_swap_v2::errors::{Erc721FunctionError, HtlcParamsError}; use crate::eth::{EthAssocTypesError, EthNftAssocTypesError, Web3RpcError}; use crate::{utxo::rpc_clients::UtxoRpcError, NumConversError, UnexpectedDerivationMethod}; use enum_derives::EnumFromStringify; use futures01::Future; use mm2_err_handle::prelude::MmError; use spv_validation::helpers_validation::SPVError; -use std::num::TryFromIntError; +use std::{array::TryFromSliceError, num::TryFromIntError}; /// Helper type used as result for swap payment validation function(s) pub type ValidatePaymentFut = Box> + Send>; @@ -24,7 +24,9 @@ pub enum ValidatePaymentError { "NumConversError", "UnexpectedDerivationMethod", "keys::Error", - "PrepareTxDataError" + "PrepareTxDataError", + "ethabi::Error", + "TryFromSliceError" )] InternalError(String), /// Problem with deserializing the transaction, or one of the transaction parts is invalid. @@ -49,8 +51,7 @@ pub enum ValidatePaymentError { WatcherRewardError(String), /// Input payment timelock overflows the type used by specific coin. TimelockOverflow(TryFromIntError), - #[display(fmt = "Nft Protocol is not supported yet!")] - NftProtocolNotSupported, + ProtocolNotSupported(String), InvalidData(String), } @@ -77,7 +78,9 @@ impl From for ValidatePaymentError { | Web3RpcError::Timeout(internal) | Web3RpcError::NumConversError(internal) | Web3RpcError::InvalidGasApiConfig(internal) => ValidatePaymentError::InternalError(internal), - Web3RpcError::NftProtocolNotSupported => ValidatePaymentError::NftProtocolNotSupported, + Web3RpcError::NftProtocolNotSupported => { + ValidatePaymentError::ProtocolNotSupported("Nft protocol is not supported".to_string()) + }, } } } diff --git a/mm2src/coins/eth.rs b/mm2src/coins/eth.rs index 15f91fd419..cd62839dfa 100644 --- a/mm2src/coins/eth.rs +++ b/mm2src/coins/eth.rs @@ -156,7 +156,12 @@ mod eip1559_gas_fee; pub(crate) use eip1559_gas_fee::FeePerGasEstimated; use eip1559_gas_fee::{BlocknativeGasApiCaller, FeePerGasSimpleEstimator, GasApiConfig, GasApiProvider, InfuraGasApiCaller}; + +pub mod erc20; +use erc20::get_token_decimals; + pub(crate) mod eth_swap_v2; +use eth_swap_v2::{EthPaymentType, PaymentMethod}; /// https://github.com/artemii235/etomic-swap/blob/master/contracts/EtomicSwap.sol /// Dev chain (195.201.137.5:8565) contract address: 0x83965C539899cC0F918552e5A26915de40ee8852 @@ -244,12 +249,51 @@ pub mod gas_limit { pub const ERC20_RECEIVER_SPEND: u64 = 150_000; /// Gas limit for swap refund tx with coins pub const ETH_SENDER_REFUND: u64 = 100_000; - /// Gas limit for swap refund tx with with ERC20 tokens + /// Gas limit for swap refund tx with ERC20 tokens pub const ERC20_SENDER_REFUND: u64 = 150_000; /// Gas limit for other operations pub const ETH_MAX_TRADE_GAS: u64 = 150_000; } +/// Default gas limits for EthGasLimitV2 +pub mod gas_limit_v2 { + /// Gas limits for maker operations in EtomicSwapMakerV2 contract + pub mod maker { + pub const ETH_PAYMENT: u64 = 65_000; + pub const ERC20_PAYMENT: u64 = 150_000; + pub const ETH_TAKER_SPEND: u64 = 100_000; + pub const ERC20_TAKER_SPEND: u64 = 150_000; + pub const ETH_MAKER_REFUND_TIMELOCK: u64 = 90_000; + pub const ERC20_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ETH_MAKER_REFUND_SECRET: u64 = 90_000; + pub const ERC20_MAKER_REFUND_SECRET: u64 = 100_000; + } + + /// Gas limits for taker operations in EtomicSwapTakerV2 contract + pub mod taker { + pub const ETH_PAYMENT: u64 = 65_000; + pub const ERC20_PAYMENT: u64 = 150_000; + pub const ETH_MAKER_SPEND: u64 = 100_000; + pub const ERC20_MAKER_SPEND: u64 = 115_000; + pub const ETH_TAKER_REFUND_TIMELOCK: u64 = 90_000; + pub const ERC20_TAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ETH_TAKER_REFUND_SECRET: u64 = 90_000; + pub const ERC20_TAKER_REFUND_SECRET: u64 = 100_000; + pub const APPROVE_PAYMENT: u64 = 50_000; + } + + pub mod nft_maker { + pub const ERC721_PAYMENT: u64 = 130_000; + pub const ERC1155_PAYMENT: u64 = 130_000; + pub const ERC721_TAKER_SPEND: u64 = 100_000; + pub const ERC1155_TAKER_SPEND: u64 = 100_000; + pub const ERC721_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ERC1155_MAKER_REFUND_TIMELOCK: u64 = 100_000; + pub const ERC721_MAKER_REFUND_SECRET: u64 = 100_000; + pub const ERC1155_MAKER_REFUND_SECRET: u64 = 100_000; + } +} + /// Coin conf param to override default gas limits #[derive(Deserialize)] #[serde(default)] @@ -268,7 +312,7 @@ pub struct EthGasLimit { pub erc20_receiver_spend: u64, /// Gas limit for swap refund tx with coins pub eth_sender_refund: u64, - /// Gas limit for swap refund tx with with ERC20 tokens + /// Gas limit for swap refund tx with ERC20 tokens pub erc20_sender_refund: u64, /// Gas limit for other operations pub eth_max_trade_gas: u64, @@ -290,6 +334,176 @@ impl Default for EthGasLimit { } } +#[derive(Default, Deserialize)] +#[serde(default)] +pub struct EthGasLimitV2 { + pub maker: MakerGasLimitV2, + pub taker: TakerGasLimitV2, + pub nft_maker: NftMakerGasLimitV2, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct MakerGasLimitV2 { + pub eth_payment: u64, + pub erc20_payment: u64, + pub eth_taker_spend: u64, + pub erc20_taker_spend: u64, + pub eth_maker_refund_timelock: u64, + pub erc20_maker_refund_timelock: u64, + pub eth_maker_refund_secret: u64, + pub erc20_maker_refund_secret: u64, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct TakerGasLimitV2 { + pub eth_payment: u64, + pub erc20_payment: u64, + pub eth_maker_spend: u64, + pub erc20_maker_spend: u64, + pub eth_taker_refund_timelock: u64, + pub erc20_taker_refund_timelock: u64, + pub eth_taker_refund_secret: u64, + pub erc20_taker_refund_secret: u64, + pub approve_payment: u64, +} + +#[derive(Deserialize)] +#[serde(default)] +pub struct NftMakerGasLimitV2 { + pub erc721_payment: u64, + pub erc1155_payment: u64, + pub erc721_taker_spend: u64, + pub erc1155_taker_spend: u64, + pub erc721_maker_refund_timelock: u64, + pub erc1155_maker_refund_timelock: u64, + pub erc721_maker_refund_secret: u64, + pub erc1155_maker_refund_secret: u64, +} + +impl EthGasLimitV2 { + fn gas_limit( + &self, + coin_type: &EthCoinType, + payment_type: EthPaymentType, + method: PaymentMethod, + ) -> Result { + match coin_type { + EthCoinType::Eth => { + let gas_limit = match payment_type { + EthPaymentType::MakerPayments => match method { + PaymentMethod::Send => self.maker.eth_payment, + PaymentMethod::Spend => self.maker.eth_taker_spend, + PaymentMethod::RefundTimelock => self.maker.eth_maker_refund_timelock, + PaymentMethod::RefundSecret => self.maker.eth_maker_refund_secret, + }, + EthPaymentType::TakerPayments => match method { + PaymentMethod::Send => self.taker.eth_payment, + PaymentMethod::Spend => self.taker.eth_maker_spend, + PaymentMethod::RefundTimelock => self.taker.eth_taker_refund_timelock, + PaymentMethod::RefundSecret => self.taker.eth_taker_refund_secret, + }, + }; + Ok(gas_limit) + }, + EthCoinType::Erc20 { .. } => { + let gas_limit = match payment_type { + EthPaymentType::MakerPayments => match method { + PaymentMethod::Send => self.maker.erc20_payment, + PaymentMethod::Spend => self.maker.erc20_taker_spend, + PaymentMethod::RefundTimelock => self.maker.erc20_maker_refund_timelock, + PaymentMethod::RefundSecret => self.maker.erc20_maker_refund_secret, + }, + EthPaymentType::TakerPayments => match method { + PaymentMethod::Send => self.taker.erc20_payment, + PaymentMethod::Spend => self.taker.erc20_maker_spend, + PaymentMethod::RefundTimelock => self.taker.erc20_taker_refund_timelock, + PaymentMethod::RefundSecret => self.taker.erc20_taker_refund_secret, + }, + }; + Ok(gas_limit) + }, + EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), + } + } + + fn nft_gas_limit(&self, contract_type: &ContractType, method: PaymentMethod) -> u64 { + match contract_type { + ContractType::Erc1155 => match method { + PaymentMethod::Send => self.nft_maker.erc1155_payment, + PaymentMethod::Spend => self.nft_maker.erc1155_taker_spend, + PaymentMethod::RefundTimelock => self.nft_maker.erc1155_maker_refund_timelock, + PaymentMethod::RefundSecret => self.nft_maker.erc1155_maker_refund_secret, + }, + ContractType::Erc721 => match method { + PaymentMethod::Send => self.nft_maker.erc721_payment, + PaymentMethod::Spend => self.nft_maker.erc721_taker_spend, + PaymentMethod::RefundTimelock => self.nft_maker.erc721_maker_refund_timelock, + PaymentMethod::RefundSecret => self.nft_maker.erc721_maker_refund_secret, + }, + } + } +} + +impl Default for MakerGasLimitV2 { + fn default() -> Self { + MakerGasLimitV2 { + eth_payment: gas_limit_v2::maker::ETH_PAYMENT, + erc20_payment: gas_limit_v2::maker::ERC20_PAYMENT, + eth_taker_spend: gas_limit_v2::maker::ETH_TAKER_SPEND, + erc20_taker_spend: gas_limit_v2::maker::ERC20_TAKER_SPEND, + eth_maker_refund_timelock: gas_limit_v2::maker::ETH_MAKER_REFUND_TIMELOCK, + erc20_maker_refund_timelock: gas_limit_v2::maker::ERC20_MAKER_REFUND_TIMELOCK, + eth_maker_refund_secret: gas_limit_v2::maker::ETH_MAKER_REFUND_SECRET, + erc20_maker_refund_secret: gas_limit_v2::maker::ERC20_MAKER_REFUND_SECRET, + } + } +} + +impl Default for TakerGasLimitV2 { + fn default() -> Self { + TakerGasLimitV2 { + eth_payment: gas_limit_v2::taker::ETH_PAYMENT, + erc20_payment: gas_limit_v2::taker::ERC20_PAYMENT, + eth_maker_spend: gas_limit_v2::taker::ETH_MAKER_SPEND, + erc20_maker_spend: gas_limit_v2::taker::ERC20_MAKER_SPEND, + eth_taker_refund_timelock: gas_limit_v2::taker::ETH_TAKER_REFUND_TIMELOCK, + erc20_taker_refund_timelock: gas_limit_v2::taker::ERC20_TAKER_REFUND_TIMELOCK, + eth_taker_refund_secret: gas_limit_v2::taker::ETH_TAKER_REFUND_SECRET, + erc20_taker_refund_secret: gas_limit_v2::taker::ERC20_TAKER_REFUND_SECRET, + approve_payment: gas_limit_v2::taker::APPROVE_PAYMENT, + } + } +} + +impl Default for NftMakerGasLimitV2 { + fn default() -> Self { + NftMakerGasLimitV2 { + erc721_payment: gas_limit_v2::nft_maker::ERC721_PAYMENT, + erc1155_payment: gas_limit_v2::nft_maker::ERC1155_PAYMENT, + erc721_taker_spend: gas_limit_v2::nft_maker::ERC721_TAKER_SPEND, + erc1155_taker_spend: gas_limit_v2::nft_maker::ERC1155_TAKER_SPEND, + erc721_maker_refund_timelock: gas_limit_v2::nft_maker::ERC721_MAKER_REFUND_TIMELOCK, + erc1155_maker_refund_timelock: gas_limit_v2::nft_maker::ERC1155_MAKER_REFUND_TIMELOCK, + erc721_maker_refund_secret: gas_limit_v2::nft_maker::ERC721_MAKER_REFUND_SECRET, + erc1155_maker_refund_secret: gas_limit_v2::nft_maker::ERC1155_MAKER_REFUND_SECRET, + } + } +} + +trait ExtractGasLimit: Default + for<'de> Deserialize<'de> { + fn key() -> &'static str; +} + +impl ExtractGasLimit for EthGasLimit { + fn key() -> &'static str { "gas_limit" } +} + +impl ExtractGasLimit for EthGasLimitV2 { + fn key() -> &'static str { "gas_limit_v2" } +} + /// Max transaction type according to EIP-2718 const ETH_MAX_TX_TYPE: u64 = 0x7f; @@ -309,20 +523,6 @@ pub type Web3RpcFut = Box> pub type Web3RpcResult = Result>; type EthPrivKeyPolicy = PrivKeyPolicy; -#[macro_export] -macro_rules! wei_from_gwei_decimal { - ($big_decimal: expr) => { - $crate::eth::wei_from_big_decimal($big_decimal, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - -#[macro_export] -macro_rules! wei_to_gwei_decimal { - ($gwei: expr) => { - $crate::eth::u256_to_big_decimal($gwei, $crate::eth::ETH_GWEI_DECIMALS) - }; -} - #[derive(Clone, Debug)] pub(crate) struct LegacyGasPrice { pub(crate) gas_price: U256, @@ -367,11 +567,11 @@ impl TryFrom for PayForGasOption { fn try_from(param: PayForGasParams) -> Result { match param { PayForGasParams::Legacy(legacy) => Ok(Self::Legacy(LegacyGasPrice { - gas_price: wei_from_gwei_decimal!(&legacy.gas_price)?, + gas_price: wei_from_gwei_decimal(&legacy.gas_price)?, })), PayForGasParams::Eip1559(eip1559) => Ok(Self::Eip1559(Eip1559FeePerGas { - max_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&eip1559.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&eip1559.max_priority_fee_per_gas)?, })), } } @@ -671,7 +871,7 @@ pub struct EthCoinImpl { /// and unlocked once the transaction is confirmed. This prevents nonce conflicts when multiple transactions /// are initiated concurrently from the same address. address_nonce_locks: Arc>>>>, - erc20_tokens_infos: Arc>>, + erc20_tokens_infos: Arc>>, /// Stores information about NFTs owned by the user. Each entry in the HashMap is uniquely identified by a composite key /// consisting of the token address and token ID, separated by a comma. This field is essential for tracking the NFT assets /// information (chain & contract type, amount etc.), where ownership and amount, in ERC1155 case, might change over time. @@ -680,6 +880,8 @@ pub struct EthCoinImpl { pub(crate) platform_fee_estimator_state: Arc, /// Config provided gas limits for swap and send transactions pub(crate) gas_limit: EthGasLimit, + /// Config provided gas limits v2 for swap v2 transactions + pub(crate) gas_limit_v2: EthGasLimitV2, /// This spawner is used to spawn coin's related futures that should be aborted on coin deactivation /// and on [`MmArc::stop`]. pub abortable_system: AbortableQueue, @@ -693,7 +895,7 @@ pub struct Web3Instance { /// Information about a token that follows the ERC20 protocol on an EVM-based network. #[derive(Clone, Debug)] -pub struct Erc20TokenInfo { +pub struct Erc20TokenDetails { /// The contract address of the token on the EVM-based network. pub token_address: Address, /// The number of decimal places the token uses. @@ -854,17 +1056,20 @@ impl EthCoinImpl { } } - pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenInfo) { + pub fn add_erc_token_info(&self, ticker: String, info: Erc20TokenDetails) { self.erc20_tokens_infos.lock().unwrap().insert(ticker, info); } /// # Warning /// Be very careful using this function since it returns dereferenced clone /// of value behind the MutexGuard and makes it non-thread-safe. - pub fn get_erc_tokens_infos(&self) -> HashMap { + pub fn get_erc_tokens_infos(&self) -> HashMap { let guard = self.erc20_tokens_infos.lock().unwrap(); (*guard).clone() } + + #[inline(always)] + pub fn chain_id(&self) -> u64 { self.chain_id } } async fn get_raw_transaction_impl(coin: EthCoin, req: RawTransactionRequest) -> RawTransactionResult { @@ -911,20 +1116,20 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit get_valid_nft_addr_to_withdraw(coin, &withdraw_type.to, &withdraw_type.token_address)?; let token_id_str = &withdraw_type.token_id.to_string(); - let wallet_amount = eth_coin.erc1155_balance(token_addr, token_id_str).await?; + let wallet_erc1155_amount = eth_coin.erc1155_balance(token_addr, token_id_str).await?; - let amount_dec = if withdraw_type.max { - wallet_amount.clone() + let amount_uint = if withdraw_type.max { + wallet_erc1155_amount.clone() } else { - withdraw_type.amount.unwrap_or_else(|| 1.into()) + withdraw_type.amount.unwrap_or_else(|| BigUint::from(1u32)) }; - if amount_dec > wallet_amount { + if amount_uint > wallet_erc1155_amount { return MmError::err(WithdrawError::NotEnoughNftsAmount { token_address: withdraw_type.token_address, token_id: withdraw_type.token_id.to_string(), - available: wallet_amount, - required: amount_dec, + available: wallet_erc1155_amount, + required: amount_uint, }); } @@ -935,7 +1140,7 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let token_id_u256 = U256::from_dec_str(token_id_str).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let amount_u256 = - U256::from_dec_str(&amount_dec.to_string()).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; + U256::from_dec_str(&amount_uint.to_string()).map_to_mm(|e| NumConversError::new(format!("{:?}", e)))?; let data = function.encode_input(&[ Token::Address(my_address), Token::Address(to_addr), @@ -987,14 +1192,14 @@ pub async fn withdraw_erc1155(ctx: MmArc, withdraw_type: WithdrawErc1155) -> Wit let fee_details = EthTxFeeDetails::new(gas, pay_for_gas_option, fee_coin)?; Ok(TransactionNftDetails { - tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + tx_hex: BytesJson::from(signed_bytes.to_vec()), // TODO: should we return tx_hex 0x-prefixed (everywhere)? + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc1155, token_address: withdraw_type.token_address, token_id: withdraw_type.token_id, - amount: amount_dec, + amount: amount_uint, fee_details: Some(fee_details.into()), coin: eth_coin.ticker.clone(), block_height: 0, @@ -1079,13 +1284,13 @@ pub async fn withdraw_erc721(ctx: MmArc, withdraw_type: WithdrawErc721) -> Withd Ok(TransactionNftDetails { tx_hex: BytesJson::from(signed_bytes.to_vec()), - tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), + tx_hash: format!("{:02x}", signed.tx_hash_as_bytes()), // TODO: add 0x hash (use unified hash format for eth wherever it is returned) from: vec![eth_coin.my_address()?], to: vec![withdraw_type.to], contract_type: ContractType::Erc721, token_address: withdraw_type.token_address, token_id: withdraw_type.token_id, - amount: 1.into(), + amount: BigUint::from(1u8), fee_details: Some(fee_details.into()), coin: eth_coin.ticker.clone(), block_height: 0, @@ -1754,7 +1959,11 @@ impl WatcherOps for EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by watchers yet".to_string(), + )) + }, } Ok(()) @@ -1995,7 +2204,11 @@ impl WatcherOps for EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by watchers yet".to_string(), + )) + }, } Ok(()) @@ -2222,7 +2435,7 @@ impl MarketCoinOps for EthCoin { let fut = async move { coin.send_raw_transaction(bytes.into()) .await - .map(|res| format!("{:02x}", res)) + .map(|res| format!("{:02x}", res)) // TODO: add 0x hash (use unified hash format for eth wherever it is returned) .map_err(|e| ERRL!("{}", e)) }; @@ -2338,18 +2551,18 @@ impl MarketCoinOps for EthCoin { Box::new(fut.boxed().compat()) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let unverified: UnverifiedTransactionWrapper = try_tx_fus!(rlp::decode(args.tx_bytes)); - let tx = try_tx_fus!(SignedEthTx::new(unverified)); + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + let unverified: UnverifiedTransactionWrapper = try_tx_s!(rlp::decode(args.tx_bytes)); + let tx = try_tx_s!(SignedEthTx::new(unverified)); let swap_contract_address = match args.swap_contract_address { - Some(addr) => try_tx_fus!(addr.try_to_address()), + Some(addr) => try_tx_s!(addr.try_to_address()), None => match tx.unsigned().action() { Call(address) => *address, Create => { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + return Err(TransactionErr::Plain(ERRL!( "Invalid payment action: the payment action cannot be create" - )))) + ))) }, }, }; @@ -2358,85 +2571,79 @@ impl MarketCoinOps for EthCoin { EthCoinType::Eth => get_function_name("ethPayment", args.watcher_reward), EthCoinType::Erc20 { .. } => get_function_name("erc20Payment", args.watcher_reward), EthCoinType::Nft { .. } => { - return Box::new(futures01::future::err(TransactionErr::ProtocolNotSupported(ERRL!( + return Err(TransactionErr::ProtocolNotSupported(ERRL!( "Nft Protocol is not supported yet!" - )))) + ))) }, }; - let payment_func = try_tx_fus!(SWAP_CONTRACT.function(&func_name)); - let decoded = try_tx_fus!(decode_contract_call(payment_func, tx.unsigned().data())); + let payment_func = try_tx_s!(SWAP_CONTRACT.function(&func_name)); + let decoded = try_tx_s!(decode_contract_call(payment_func, tx.unsigned().data())); let id = match decoded.first() { Some(Token::FixedBytes(bytes)) => bytes.clone(), invalid_token => { - return Box::new(futures01::future::err(TransactionErr::Plain(ERRL!( + return Err(TransactionErr::Plain(ERRL!( "Expected Token::FixedBytes, got {:?}", invalid_token - )))) + ))) }, }; - let selfi = self.clone(); - let from_block = args.from_block; - let wait_until = args.wait_until; - let check_every = args.check_every; - let fut = async move { - loop { - if now_sec() > wait_until { - return TX_PLAIN_ERR!( - "Waited too long until {} for transaction {:?} to be spent ", - wait_until, - tx, - ); - } - let current_block = match selfi.current_block().compat().await { - Ok(b) => b, - Err(e) => { - error!("Error getting block number: {}", e); - Timer::sleep(5.).await; - continue; - }, - }; + loop { + if now_sec() > args.wait_until { + return TX_PLAIN_ERR!( + "Waited too long until {} for transaction {:?} to be spent ", + args.wait_until, + tx, + ); + } - let events = match selfi - .spend_events(swap_contract_address, from_block, current_block) - .compat() - .await - { - Ok(ev) => ev, - Err(e) => { - error!("Error getting spend events: {}", e); - Timer::sleep(5.).await; - continue; - }, - }; + let current_block = match self.current_block().compat().await { + Ok(b) => b, + Err(e) => { + error!("Error getting block number: {}", e); + Timer::sleep(5.).await; + continue; + }, + }; - let found = events.iter().find(|event| &event.data.0[..32] == id.as_slice()); + let events = match self + .spend_events(swap_contract_address, args.from_block, current_block) + .compat() + .await + { + Ok(ev) => ev, + Err(e) => { + error!("Error getting spend events: {}", e); + Timer::sleep(5.).await; + continue; + }, + }; - if let Some(event) = found { - if let Some(tx_hash) = event.transaction_hash { - let transaction = match selfi.transaction(TransactionId::Hash(tx_hash)).await { - Ok(Some(t)) => t, - Ok(None) => { - info!("Tx {} not found yet", tx_hash); - Timer::sleep(check_every).await; - continue; - }, - Err(e) => { - error!("Get tx {} error: {}", tx_hash, e); - Timer::sleep(check_every).await; - continue; - }, - }; + let found = events.iter().find(|event| &event.data.0[..32] == id.as_slice()); - return Ok(TransactionEnum::from(try_tx_s!(signed_tx_from_web3_tx(transaction)))); - } - } + if let Some(event) = found { + if let Some(tx_hash) = event.transaction_hash { + let transaction = match self.transaction(TransactionId::Hash(tx_hash)).await { + Ok(Some(t)) => t, + Ok(None) => { + info!("Tx {} not found yet", tx_hash); + Timer::sleep(args.check_every).await; + continue; + }, + Err(e) => { + error!("Get tx {} error: {}", tx_hash, e); + Timer::sleep(args.check_every).await; + continue; + }, + }; - Timer::sleep(5.).await; + return Ok(TransactionEnum::from(try_tx_s!(signed_tx_from_web3_tx(transaction)))); + } } - }; - Box::new(fut.boxed().compat()) + + Timer::sleep(5.).await; + } } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { @@ -4405,7 +4612,7 @@ impl EthCoin { self.get_token_balance_for_address(my_address, token_address).await } - async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult { + async fn erc1155_balance(&self, token_addr: Address, token_id: &str) -> MmResult { let wallet_amount_uint = match self.coin_type { EthCoinType::Eth | EthCoinType::Nft { .. } => { let function = ERC1155_CONTRACT.function("balanceOf")?; @@ -4431,7 +4638,8 @@ impl EthCoin { )) }, }; - let wallet_amount = u256_to_big_decimal(wallet_amount_uint, self.decimals)?; + // The "balanceOf" function in ERC1155 standard returns the exact count of tokens held by address without any decimals or scaling factors + let wallet_amount = wallet_amount_uint.to_string().parse::()?; Ok(wallet_amount) } @@ -4536,7 +4744,7 @@ impl EthCoin { self.call(request, Some(BlockId::Number(BlockNumber::Latest))).await } - fn allowance(&self, spender: Address) -> Web3RpcFut { + pub fn allowance(&self, spender: Address) -> Web3RpcFut { let coin = self.clone(); let fut = async move { match coin.coin_type { @@ -4601,7 +4809,7 @@ impl EthCoin { Box::new(fut.boxed().compat()) } - fn approve(&self, spender: Address, amount: U256) -> EthTxFut { + pub fn approve(&self, spender: Address, amount: U256) -> EthTxFut { let coin = self.clone(); let fut = async move { let token_addr = match coin.coin_type { @@ -4609,7 +4817,7 @@ impl EthCoin { EthCoinType::Erc20 { token_addr, .. } => token_addr, EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "Nft Protocol is not supported yet!" + "Nft Protocol is not supported by 'approve'!" ))) }, }; @@ -4949,7 +5157,11 @@ impl EthCoin { ))); } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported by legacy swap".to_string(), + )) + }, } Ok(()) @@ -5886,7 +6098,11 @@ fn validate_fee_impl(coin: EthCoin, validate_fee_args: EthValidateFeeArgs<'_>) - }, } }, - EthCoinType::Nft { .. } => return MmError::err(ValidatePaymentError::NftProtocolNotSupported), + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "Nft protocol is not supported".to_string(), + )) + }, } Ok(()) @@ -5959,6 +6175,12 @@ pub fn wei_from_big_decimal(amount: &BigDecimal, decimals: u8) -> NumConversResu U256::from_dec_str(&amount).map_to_mm(|e| NumConversError::new(format!("{:?}", e))) } +pub fn wei_from_gwei_decimal(bigdec: &BigDecimal) -> NumConversResult { + wei_from_big_decimal(bigdec, ETH_GWEI_DECIMALS) +} + +pub fn wei_to_gwei_decimal(wei: U256) -> NumConversResult { u256_to_big_decimal(wei, ETH_GWEI_DECIMALS) } + impl Transaction for SignedEthTx { fn tx_hex(&self) -> Vec { rlp::encode(self).to_vec() } @@ -6086,32 +6308,6 @@ fn signed_tx_from_web3_tx(transaction: Web3Transaction) -> Result, token_addr: Address) -> Result { - let function = try_s!(ERC20_CONTRACT.function("decimals")); - let data = try_s!(function.encode_input(&[])); - let request = CallRequest { - from: Some(Address::default()), - to: Some(token_addr), - gas: None, - gas_price: None, - value: Some(0.into()), - data: Some(data.into()), - ..CallRequest::default() - }; - - let res = web3 - .eth() - .call(request, Some(BlockId::Number(BlockNumber::Latest))) - .map_err(|e| ERRL!("{}", e)) - .await?; - let tokens = try_s!(function.decode_output(&res.0)); - let decimals = match tokens[0] { - Token::Uint(dec) => dec.as_u64(), - _ => return ERR!("Invalid decimals type {:?}", tokens), - }; - Ok(decimals as u8) -} - pub fn valid_addr_from_str(addr_str: &str) -> Result { let addr = try_s!(addr_from_str(addr_str)); if !is_valid_checksum_addr(addr_str) { @@ -6366,7 +6562,8 @@ pub async fn eth_coin_from_conf_and_request( let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(conf)?; + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf)?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(conf)?; let coin = EthCoinImpl { priv_key_policy: key_pair, @@ -6393,6 +6590,7 @@ pub async fn eth_coin_from_conf_and_request( nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; @@ -7018,11 +7216,12 @@ pub fn pubkey_from_extended(extended_pubkey: &Secp256k1ExtendedPublicKey) -> Pub pubkey_uncompressed } -fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { - if coin_conf["gas_limit"].is_null() { +fn extract_gas_limit_from_conf(coin_conf: &Json) -> Result { + let key = T::key(); + if coin_conf[key].is_null() { Ok(Default::default()) } else { - json::from_value(coin_conf["gas_limit"].clone()).map_err(|e| e.to_string()) + json::from_value(coin_conf[key].clone()).map_err(|e| e.to_string()) } } @@ -7146,7 +7345,7 @@ impl TakerCoinSwapOpsV2 for EthCoin { taker_payment: &Self::Tx, _from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { self.wait_for_taker_payment_spend_impl(taker_payment, wait_until).await } } @@ -7209,8 +7408,38 @@ impl EthCoin { nfts_infos: Arc::clone(&self.nfts_infos), platform_fee_estimator_state: Arc::clone(&self.platform_fee_estimator_state), gas_limit: EthGasLimit::default(), + gas_limit_v2: EthGasLimitV2::default(), abortable_system: self.abortable_system.create_subsystem().unwrap(), }; EthCoin(Arc::new(coin)) } } + +#[async_trait] +impl MakerCoinSwapOpsV2 for EthCoin { + async fn send_maker_payment_v2(&self, args: SendMakerPaymentArgs<'_, Self>) -> Result { + self.send_maker_payment_v2_impl(args).await + } + + async fn validate_maker_payment_v2(&self, args: ValidateMakerPaymentArgs<'_, Self>) -> ValidatePaymentResult<()> { + self.validate_maker_payment_v2_impl(args).await + } + + async fn refund_maker_payment_v2_timelock( + &self, + args: RefundMakerPaymentTimelockArgs<'_>, + ) -> Result { + self.refund_maker_payment_v2_timelock_impl(args).await + } + + async fn refund_maker_payment_v2_secret( + &self, + args: RefundMakerPaymentSecretArgs<'_, Self>, + ) -> Result { + self.refund_maker_payment_v2_secret_impl(args).await + } + + async fn spend_maker_payment_v2(&self, args: SpendMakerPaymentArgs<'_, Self>) -> Result { + self.spend_maker_payment_v2_impl(args).await + } +} diff --git a/mm2src/coins/eth/eip1559_gas_fee.rs b/mm2src/coins/eth/eip1559_gas_fee.rs index 4d33781f39..32ac186169 100644 --- a/mm2src/coins/eth/eip1559_gas_fee.rs +++ b/mm2src/coins/eth/eip1559_gas_fee.rs @@ -1,8 +1,8 @@ //! Provides estimations of base and priority fee per gas or fetch estimations from a gas api provider use super::web3_transport::FeeHistoryResult; -use super::{Web3RpcError, Web3RpcResult}; -use crate::{wei_from_gwei_decimal, wei_to_gwei_decimal, EthCoin, NumConversError}; +use super::{wei_from_gwei_decimal, wei_to_gwei_decimal, Web3RpcError, Web3RpcResult}; +use crate::{EthCoin, NumConversError}; use ethereum_types::U256; use mm2_err_handle::mm_error::MmError; use mm2_err_handle::or_mm_error::OrMmError; @@ -104,24 +104,24 @@ impl TryFrom for FeePerGasEstimated { fn try_from(infura_fees: InfuraFeePerGas) -> Result { Ok(Self { - base_fee: wei_from_gwei_decimal!(&infura_fees.estimated_base_fee)?, + base_fee: wei_from_gwei_decimal(&infura_fees.estimated_base_fee)?, low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.low.suggested_max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.low.suggested_max_priority_fee_per_gas)?, min_wait_time: Some(infura_fees.low.min_wait_time_estimate), max_wait_time: Some(infura_fees.low.max_wait_time_estimate), }, medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.medium.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &infura_fees.medium.suggested_max_priority_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.medium.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal( + &infura_fees.medium.suggested_max_priority_fee_per_gas, )?, min_wait_time: Some(infura_fees.medium.min_wait_time_estimate), max_wait_time: Some(infura_fees.medium.max_wait_time_estimate), }, high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_fee_per_gas)?, - max_priority_fee_per_gas: wei_from_gwei_decimal!(&infura_fees.high.suggested_max_priority_fee_per_gas)?, + max_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_fee_per_gas)?, + max_priority_fee_per_gas: wei_from_gwei_decimal(&infura_fees.high.suggested_max_priority_fee_per_gas)?, min_wait_time: Some(infura_fees.high.min_wait_time_estimate), max_wait_time: Some(infura_fees.high.max_wait_time_estimate), }, @@ -143,33 +143,33 @@ impl TryFrom for FeePerGasEstimated { return Ok(FeePerGasEstimated::default()); } Ok(Self { - base_fee: wei_from_gwei_decimal!(&block_prices.block_prices[0].base_fee_per_gas)?, + base_fee: wei_from_gwei_decimal(&block_prices.block_prices[0].base_fee_per_gas)?, low: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[2].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, }, medium: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[1].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, }, high: FeePerGasLevel { - max_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas + max_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_fee_per_gas, )?, - max_priority_fee_per_gas: wei_from_gwei_decimal!( - &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas + max_priority_fee_per_gas: wei_from_gwei_decimal( + &block_prices.block_prices[0].estimated_prices[0].max_priority_fee_per_gas, )?, min_wait_time: None, max_wait_time: None, @@ -260,7 +260,7 @@ impl FeePerGasSimpleEstimator { let max_priority_fee_per_gas = Self::percentile_of(&level_rewards, Self::PRIORITY_FEE_PERCENTILES[level_index]); // Convert the priority fee to BigDecimal gwei, falling back to 0 on error. let max_priority_fee_per_gas_gwei = - wei_to_gwei_decimal!(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); + wei_to_gwei_decimal(max_priority_fee_per_gas).unwrap_or_else(|_| BigDecimal::from(0)); // Calculate the max fee per gas by adjusting the base fee and adding the priority fee. let adjust_max_fee = @@ -273,7 +273,7 @@ impl FeePerGasSimpleEstimator { Ok(FeePerGasLevel { max_priority_fee_per_gas, - max_fee_per_gas: wei_from_gwei_decimal!(&max_fee_per_gas_dec)?, + max_fee_per_gas: wei_from_gwei_decimal(&max_fee_per_gas_dec)?, // TODO: Consider adding default wait times if applicable (and mark them as uncertain). min_wait_time: None, max_wait_time: None, @@ -290,7 +290,7 @@ impl FeePerGasSimpleEstimator { .first() .cloned() .unwrap_or_else(|| U256::from(0)); - let latest_base_fee_dec = wei_to_gwei_decimal!(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); + let latest_base_fee_dec = wei_to_gwei_decimal(latest_base_fee).unwrap_or_else(|_| BigDecimal::from(0)); // The predicted base fee is not used for calculating eip1559 values here and is provided for other purposes // (f.e if the caller would like to do own estimates of max fee and max priority fee) diff --git a/mm2src/coins/eth/erc20.rs b/mm2src/coins/eth/erc20.rs new file mode 100644 index 0000000000..75f7033fda --- /dev/null +++ b/mm2src/coins/eth/erc20.rs @@ -0,0 +1,107 @@ +use crate::eth::web3_transport::Web3Transport; +use crate::eth::{EthCoin, ERC20_CONTRACT}; +use crate::{CoinsContext, MmCoinEnum}; +use ethabi::Token; +use ethereum_types::Address; +use futures_util::TryFutureExt; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmResult; +use web3::types::{BlockId, BlockNumber, CallRequest}; +use web3::{Transport, Web3}; + +async fn call_erc20_function( + web3: &Web3, + token_addr: Address, + function_name: &str, +) -> Result, String> { + let function = try_s!(ERC20_CONTRACT.function(function_name)); + let data = try_s!(function.encode_input(&[])); + let request = CallRequest { + from: Some(Address::default()), + to: Some(token_addr), + gas: None, + gas_price: None, + value: Some(0.into()), + data: Some(data.into()), + ..CallRequest::default() + }; + + let res = web3 + .eth() + .call(request, Some(BlockId::Number(BlockNumber::Latest))) + .map_err(|e| ERRL!("{}", e)) + .await?; + function.decode_output(&res.0).map_err(|e| ERRL!("{}", e)) +} + +pub(crate) async fn get_token_decimals(web3: &Web3, token_addr: Address) -> Result { + let tokens = call_erc20_function(web3, token_addr, "decimals").await?; + let Some(token) = tokens.into_iter().next() else { + return ERR!("No value returned from decimals() call"); + }; + let Token::Uint(dec) = token else { + return ERR!("Expected Uint token for decimals, got {:?}", token); + }; + Ok(dec.as_u64() as u8) +} + +async fn get_token_symbol(coin: &EthCoin, token_addr: Address) -> Result { + let web3 = try_s!(coin.web3().await); + let tokens = call_erc20_function(&web3, token_addr, "symbol").await?; + let Some(token) = tokens.into_iter().next() else { + return ERR!("No value returned from symbol() call"); + }; + let Token::String(symbol) = token else { + return ERR!("Expected String token for symbol, got {:?}", token); + }; + Ok(symbol) +} + +#[derive(Serialize)] +pub struct Erc20TokenInfo { + pub symbol: String, + pub decimals: u8, +} + +pub async fn get_erc20_token_info(coin: &EthCoin, token_addr: Address) -> Result { + let symbol = get_token_symbol(coin, token_addr).await?; + let web3 = try_s!(coin.web3().await); + let decimals = get_token_decimals(&web3, token_addr).await?; + Ok(Erc20TokenInfo { symbol, decimals }) +} + +/// Finds if an ERC20 token is in coins config by its contract address and returns its ticker. +pub fn get_erc20_ticker_by_contract_address(ctx: &MmArc, platform: &str, contract_address: &str) -> Option { + ctx.conf["coins"].as_array()?.iter().find_map(|coin| { + let protocol = coin.get("protocol")?; + let protocol_type = protocol.get("type")?.as_str()?; + if protocol_type != "ERC20" { + return None; + } + let protocol_data = protocol.get("protocol_data")?; + let coin_platform = protocol_data.get("platform")?.as_str()?; + let coin_contract_address = protocol_data.get("contract_address")?.as_str()?; + + if coin_platform == platform && coin_contract_address == contract_address { + coin.get("coin")?.as_str().map(|s| s.to_string()) + } else { + None + } + }) +} + +/// Finds an enabled ERC20 token by its contract address and returns it as `MmCoinEnum`. +pub async fn get_enabled_erc20_by_contract( + ctx: &MmArc, + contract_address: Address, +) -> MmResult, String> { + let cctx = CoinsContext::from_ctx(ctx)?; + let coins = cctx.coins.lock().await; + + Ok(coins.values().find_map(|coin| match &coin.inner { + MmCoinEnum::EthCoin(eth_coin) if eth_coin.erc20_token_address() == Some(contract_address) => { + Some(coin.inner.clone()) + }, + _ => None, + })) +} diff --git a/mm2src/coins/eth/eth_balance_events.rs b/mm2src/coins/eth/eth_balance_events.rs index 231aa68507..0cc798ad7e 100644 --- a/mm2src/coins/eth/eth_balance_events.rs +++ b/mm2src/coins/eth/eth_balance_events.rs @@ -14,7 +14,7 @@ use mm2_number::BigDecimal; use std::collections::{HashMap, HashSet}; use super::EthCoin; -use crate::{eth::{u256_to_big_decimal, Erc20TokenInfo}, +use crate::{eth::{u256_to_big_decimal, Erc20TokenDetails}, BalanceError, CoinWithDerivationMethod, MmCoin}; struct BalanceData { @@ -40,9 +40,9 @@ async fn get_all_balance_results_concurrently(coin: &EthCoin, addresses: HashSet // // Unlike tokens, the platform coin length is constant (=1). Instead of creating a generic // type and mapping the platform coin and the entire token list (which can grow at any time), we map - // the platform coin to Erc20TokenInfo so that we can use the token list right away without + // the platform coin to Erc20TokenDetails so that we can use the token list right away without // additional mapping. - tokens.insert(coin.ticker.clone(), Erc20TokenInfo { + tokens.insert(coin.ticker.clone(), Erc20TokenDetails { // This is a dummy value, since there is no token address for the platform coin. // In the fetch_balance function, we check if the token_ticker is equal to this // coin's ticker to avoid using token_address to fetch the balance @@ -72,7 +72,7 @@ async fn fetch_balance( coin: &EthCoin, address: Address, token_ticker: String, - info: &Erc20TokenInfo, + info: &Erc20TokenDetails, ) -> Result { let (balance_as_u256, decimals) = if token_ticker == coin.ticker { ( diff --git a/mm2src/coins/eth/eth_rpc.rs b/mm2src/coins/eth/eth_rpc.rs index 922e219fbd..3dc6711126 100644 --- a/mm2src/coins/eth/eth_rpc.rs +++ b/mm2src/coins/eth/eth_rpc.rs @@ -241,8 +241,8 @@ impl EthCoin { .and_then(|t| serde_json::from_value(t).map_err(Into::into)) } - /// Get chain id - pub(crate) async fn chain_id(&self) -> Result { + /// Get chain id from network + pub(crate) async fn network_chain_id(&self) -> Result { self.try_rpc_send("eth_chainId", vec![]) .await .and_then(|t| serde_json::from_value(t).map_err(Into::into)) diff --git a/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs new file mode 100644 index 0000000000..3089604ede --- /dev/null +++ b/mm2src/coins/eth/eth_swap_v2/eth_maker_swap_v2.rs @@ -0,0 +1,486 @@ +use super::{validate_amount, validate_from_to_and_status, EthPaymentType, PaymentMethod, PrepareTxDataError, + ZERO_VALUE}; +use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; +use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, + MakerPaymentStateV2, SignedEthTx, MAKER_SWAP_V2}; +use crate::{ParseCoinAssocTypes, RefundMakerPaymentSecretArgs, RefundMakerPaymentTimelockArgs, SendMakerPaymentArgs, + SpendMakerPaymentArgs, SwapTxTypeWithSecretHash, TransactionErr, ValidateMakerPaymentArgs}; +use ethabi::{Function, Token}; +use ethcore_transaction::Action; +use ethereum_types::{Address, Public, U256}; +use ethkey::public_to_address; +use futures::compat::Future01CompatExt; +use mm2_err_handle::mm_error::MmError; +use mm2_err_handle::prelude::MapToMmResult; +use std::convert::TryInto; +use web3::types::TransactionId; + +const ETH_MAKER_PAYMENT: &str = "ethMakerPayment"; +const ERC20_MAKER_PAYMENT: &str = "erc20MakerPayment"; + +/// state index for `MakerPayment` structure from `EtomicSwapMakerV2.sol` +/// +/// struct MakerPayment { +/// bytes20 paymentHash; +/// uint32 paymentLockTime; +/// MakerPaymentState state; +/// } +const MAKER_PAYMENT_STATE_INDEX: usize = 2; + +struct MakerPaymentArgs { + taker_address: Address, + taker_secret_hash: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, +} + +struct MakerValidationArgs<'a> { + swap_id: Vec, + amount: U256, + taker: Address, + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], + payment_time_lock: u64, +} + +struct MakerRefundTimelockArgs { + payment_amount: U256, + taker_address: Address, + taker_secret_hash: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +struct MakerRefundSecretArgs { + payment_amount: U256, + taker_address: Address, + taker_secret: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +impl EthCoin { + pub(crate) async fn send_maker_payment_v2_impl( + &self, + args: SendMakerPaymentArgs<'_, Self>, + ) -> Result { + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + let payment_args = { + let taker_address = public_to_address(args.taker_pub); + MakerPaymentArgs { + taker_address, + taker_secret_hash: try_tx_s!(args.taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(args.maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + } + }; + match &self.coin_type { + EthCoinType::Eth => { + let data = try_tx_s!(self.prepare_maker_eth_payment_data(&payment_args).await); + self.sign_and_send_transaction( + payment_amount, + Action::Call(maker_swap_v2_contract), + data, + U256::from(self.gas_limit_v2.maker.eth_payment), + ) + .compat() + .await + }, + EthCoinType::Erc20 { + platform: _, + token_addr, + } => { + let data = try_tx_s!( + self.prepare_maker_erc20_payment_data(&payment_args, payment_amount, *token_addr) + .await + ); + self.handle_allowance(maker_swap_v2_contract, payment_amount, args.time_lock) + .await?; + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(self.gas_limit_v2.maker.erc20_payment), + ) + .compat() + .await + }, + EthCoinType::Nft { .. } => Err(TransactionErr::ProtocolNotSupported(ERRL!( + "NFT protocol is not supported for ETH and ERC20 Swaps" + ))), + } + } + + pub(crate) async fn validate_maker_payment_v2_impl( + &self, + args: ValidateMakerPaymentArgs<'_, Self>, + ) -> ValidatePaymentResult<()> { + if let EthCoinType::Nft { .. } = self.coin_type { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + } + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + ValidatePaymentError::InternalError("Expected swap_v2_contracts to be Some, but found None".to_string()) + })? + .maker_swap_v2_contract; + let taker_secret_hash = args.taker_secret_hash.try_into()?; + let maker_secret_hash = args.maker_secret_hash.try_into()?; + validate_amount(&args.amount).map_to_mm(ValidatePaymentError::InternalError)?; + let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); + let maker_status = self + .payment_status_v2( + maker_swap_v2_contract, + Token::FixedBytes(swap_id.clone()), + &MAKER_SWAP_V2, + EthPaymentType::MakerPayments, + MAKER_PAYMENT_STATE_INDEX, + ) + .await?; + + let tx_from_rpc = self + .transaction(TransactionId::Hash(args.maker_payment_tx.tx_hash())) + .await?; + let tx_from_rpc = tx_from_rpc.as_ref().ok_or_else(|| { + ValidatePaymentError::TxDoesNotExist(format!( + "Didn't find provided tx {:?} on ETH node", + args.maker_payment_tx.tx_hash() + )) + })?; + let maker_address = public_to_address(args.maker_pub); + validate_from_to_and_status( + tx_from_rpc, + maker_address, + maker_swap_v2_contract, + maker_status, + MakerPaymentStateV2::PaymentSent as u8, + )?; + + let validation_args = { + let amount = wei_from_big_decimal(&args.amount, self.decimals)?; + MakerValidationArgs { + swap_id, + amount, + taker: self.my_addr().await, + taker_secret_hash, + maker_secret_hash, + payment_time_lock: args.time_lock, + } + }; + match self.coin_type { + EthCoinType::Eth => { + let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; + validate_eth_maker_payment_data(&decoded, &validation_args, function, tx_from_rpc.value)?; + }, + EthCoinType::Erc20 { token_addr, .. } => { + let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; + let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; + validate_erc20_maker_payment_data(&decoded, &validation_args, function, token_addr)?; + }, + EthCoinType::Nft { .. } => { + return MmError::err(ValidatePaymentError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + }, + } + Ok(()) + } + + pub(crate) async fn refund_maker_payment_v2_timelock_impl( + &self, + args: RefundMakerPaymentTimelockArgs<'_>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::MakerPayments, + PaymentMethod::RefundTimelock, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let (maker_secret_hash, taker_secret_hash) = match args.tx_type_with_secret_hash { + SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash, + taker_secret_hash, + } => (maker_secret_hash, taker_secret_hash), + _ => { + return Err(TransactionErr::Plain(ERRL!( + "Unsupported swap tx type for timelock refund" + ))) + }, + }; + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + + let args = { + let taker_address = public_to_address(&Public::from_slice(args.taker_pub)); + MakerRefundTimelockArgs { + payment_amount, + taker_address, + taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + token_address, + } + }; + let data = try_tx_s!(self.prepare_refund_maker_payment_timelock_data(args).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + pub(crate) async fn refund_maker_payment_v2_secret_impl( + &self, + args: RefundMakerPaymentSecretArgs<'_, Self>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::MakerPayments, + PaymentMethod::RefundSecret, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let taker_secret = try_tx_s!(args.taker_secret.try_into()); + let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); + let payment_amount = try_tx_s!(wei_from_big_decimal(&args.amount, self.decimals)); + let args = { + let taker_address = public_to_address(args.taker_pub); + MakerRefundSecretArgs { + payment_amount, + taker_address, + taker_secret, + maker_secret_hash, + payment_time_lock: args.time_lock, + token_address, + } + }; + let data = try_tx_s!(self.prepare_refund_maker_payment_secret_data(args).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + pub(crate) async fn spend_maker_payment_v2_impl( + &self, + args: SpendMakerPaymentArgs<'_, Self>, + ) -> Result { + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .maker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::MakerPayments, PaymentMethod::Spend) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + let data = try_tx_s!(self.prepare_spend_maker_payment_data(args, token_address).await); + + self.sign_and_send_transaction( + U256::from(ZERO_VALUE), + Action::Call(maker_swap_v2_contract), + data, + U256::from(gas_limit), + ) + .compat() + .await + } + + /// Prepares data for EtomicSwapMakerV2 contract [ethMakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L30) method + async fn prepare_maker_eth_payment_data(&self, args: &MakerPaymentArgs) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function(ETH_MAKER_PAYMENT)?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Uint(args.payment_time_lock.into()), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [erc20MakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L64) method + async fn prepare_maker_erc20_payment_data( + &self, + args: &MakerPaymentArgs, + payment_amount: U256, + token_address: Address, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function(ERC20_MAKER_PAYMENT)?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(payment_amount), + Token::Address(token_address), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Uint(args.payment_time_lock.into()), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L144) method + async fn prepare_refund_maker_payment_timelock_data( + &self, + args: MakerRefundTimelockArgs, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("refundMakerPaymentTimelock")?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(args.payment_amount), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Address(args.token_address), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [refundMakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L190) method + async fn prepare_refund_maker_payment_secret_data( + &self, + args: MakerRefundSecretArgs, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("refundMakerPaymentSecret")?; + let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(args.payment_amount), + Token::Address(args.taker_address), + Token::FixedBytes(args.taker_secret.to_vec()), + Token::FixedBytes(args.maker_secret_hash.to_vec()), + Token::Address(args.token_address), + ])?; + Ok(data) + } + + /// Prepares data for EtomicSwapMakerV2 contract [spendMakerPayment](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapMakerV2.sol#L104) method + async fn prepare_spend_maker_payment_data( + &self, + args: SpendMakerPaymentArgs<'_, Self>, + token_address: Address, + ) -> Result, PrepareTxDataError> { + let function = MAKER_SWAP_V2.function("spendMakerPayment")?; + let id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); + let maker_address = public_to_address(args.maker_pub); + let payment_amount = wei_from_big_decimal(&args.amount, self.decimals) + .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; + let data = function.encode_input(&[ + Token::FixedBytes(id), + Token::Uint(payment_amount), + Token::Address(maker_address), + Token::FixedBytes(args.taker_secret_hash.to_vec()), + Token::FixedBytes(args.maker_secret.to_vec()), + Token::Address(token_address), + ])?; + Ok(data) + } +} + +/// Validation function for ETH maker payment data +fn validate_eth_maker_payment_data( + decoded: &[Token], + args: &MakerValidationArgs, + func: &Function, + tx_value: U256, +) -> Result<(), MmError> { + let checks = vec![ + (0, Token::FixedBytes(args.swap_id.clone()), "id"), + (1, Token::Address(args.taker), "taker"), + (2, Token::FixedBytes(args.taker_secret_hash.to_vec()), "takerSecretHash"), + (3, Token::FixedBytes(args.maker_secret_hash.to_vec()), "makerSecretHash"), + (4, Token::Uint(U256::from(args.payment_time_lock)), "paymentLockTime"), + ]; + + for (index, expected_token, field_name) in checks { + let token = get_function_input_data(decoded, func, index).map_to_mm(ValidatePaymentError::InternalError)?; + if token != expected_token { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ETH Maker Payment `{}` {:?} is invalid, expected {:?}", + field_name, + decoded.get(index), + expected_token + ))); + } + } + if args.amount != tx_value { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ETH Maker Payment amount, is invalid, expected {:?}, got {:?}", + args.amount, tx_value + ))); + } + Ok(()) +} + +/// Validation function for ERC20 maker payment data +fn validate_erc20_maker_payment_data( + decoded: &[Token], + args: &MakerValidationArgs, + func: &Function, + token_addr: Address, +) -> Result<(), MmError> { + let checks = vec![ + (0, Token::FixedBytes(args.swap_id.clone()), "id"), + (1, Token::Uint(args.amount), "amount"), + (2, Token::Address(token_addr), "tokenAddress"), + (3, Token::Address(args.taker), "taker"), + (4, Token::FixedBytes(args.taker_secret_hash.to_vec()), "takerSecretHash"), + (5, Token::FixedBytes(args.maker_secret_hash.to_vec()), "makerSecretHash"), + (6, Token::Uint(U256::from(args.payment_time_lock)), "paymentLockTime"), + ]; + + for (index, expected_token, field_name) in checks { + let token = get_function_input_data(decoded, func, index).map_to_mm(ValidatePaymentError::InternalError)?; + if token != expected_token { + return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( + "ERC20 Maker Payment `{}` {:?} is invalid, expected {:?}", + field_name, + decoded.get(index), + expected_token + ))); + } + } + Ok(()) +} diff --git a/mm2src/coins/eth/eth_swap_v2.rs b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs similarity index 71% rename from mm2src/coins/eth/eth_swap_v2.rs rename to mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs index 0d61ac6004..fea91b0408 100644 --- a/mm2src/coins/eth/eth_swap_v2.rs +++ b/mm2src/coins/eth/eth_swap_v2/eth_taker_swap_v2.rs @@ -1,31 +1,36 @@ -use super::eth::{wei_from_big_decimal, EthCoin, EthCoinType, SignedEthTx, TAKER_SWAP_V2}; -use super::{decode_contract_call, get_function_input_data, ParseCoinAssocTypes, RefundFundingSecretArgs, - RefundTakerPaymentArgs, SendTakerFundingArgs, SwapTxTypeWithSecretHash, TakerPaymentStateV2, Transaction, - TransactionErr, ValidateSwapV2TxError, ValidateSwapV2TxResult, ValidateTakerFundingArgs}; +use super::{check_decoded_length, validate_amount, validate_from_to_and_status, validate_payment_state, + EthPaymentType, PaymentMethod, PrepareTxDataError, ZERO_VALUE}; +use crate::eth::{decode_contract_call, get_function_input_data, wei_from_big_decimal, EthCoin, EthCoinType, + ParseCoinAssocTypes, RefundFundingSecretArgs, RefundTakerPaymentArgs, SendTakerFundingArgs, + SignedEthTx, SwapTxTypeWithSecretHash, TakerPaymentStateV2, TransactionErr, ValidateSwapV2TxError, + ValidateSwapV2TxResult, ValidateTakerFundingArgs, TAKER_SWAP_V2}; use crate::{FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, SearchForFundingSpendErr, - WaitForTakerPaymentSpendError}; + WaitForPaymentSpendError}; use common::executor::Timer; use common::now_sec; -use enum_derives::EnumFromStringify; -use ethabi::{Contract, Function, Token}; +use ethabi::{Function, Token}; use ethcore_transaction::Action; use ethereum_types::{Address, Public, U256}; use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; -use mm2_number::BigDecimal; use std::convert::TryInto; -use web3::types::{Transaction as Web3Tx, TransactionId}; +use web3::types::TransactionId; -/// ZERO_VALUE is used to represent a 0 amount in transactions where the value is encoded in the transaction input data. -/// This is typically used in function calls where the value is not directly transferred with the transaction, such as in -/// `spendTakerPayment` where the [amount](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L166) -/// is provided as part of the input data rather than as an Ether value -pub(crate) const ZERO_VALUE: u32 = 0; const ETH_TAKER_PAYMENT: &str = "ethTakerPayment"; const ERC20_TAKER_PAYMENT: &str = "erc20TakerPayment"; const TAKER_PAYMENT_APPROVE: &str = "takerPaymentApprove"; +/// state index for `TakerPayment` structure from `EtomicSwapTakerV2.sol` +/// +/// struct TakerPayment { +/// bytes20 paymentHash; +/// uint32 preApproveLockTime; +/// uint32 paymentLockTime; +/// TakerPaymentState state; +/// } +const TAKER_PAYMENT_STATE_INDEX: usize = 3; + struct TakerFundingArgs { dex_fee: U256, payment_amount: U256, @@ -36,17 +41,37 @@ struct TakerFundingArgs { payment_time_lock: u64, } -struct TakerRefundArgs { +struct TakerRefundTimelockArgs { dex_fee: U256, payment_amount: U256, maker_address: Address, - taker_secret: [u8; 32], taker_secret_hash: [u8; 32], maker_secret_hash: [u8; 32], payment_time_lock: u64, token_address: Address, } +struct TakerRefundSecretArgs { + dex_fee: U256, + payment_amount: U256, + maker_address: Address, + taker_secret: [u8; 32], + maker_secret_hash: [u8; 32], + payment_time_lock: u64, + token_address: Address, +} + +struct TakerValidationArgs<'a> { + swap_id: Vec, + amount: U256, + dex_fee: U256, + receiver: Address, + taker_secret_hash: &'a [u8; 32], + maker_secret_hash: &'a [u8; 32], + funding_time_lock: u64, + payment_time_lock: u64, +} + impl EthCoin { /// Calls `"ethTakerPayment"` or `"erc20TakerPayment"` swap contract methods. /// Returns taker sent payment transaction. @@ -56,9 +81,8 @@ impl EthCoin { ) -> Result { let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; // TODO add burnFee support let dex_fee = try_tx_s!(wei_from_big_decimal(&args.dex_fee.fee_amount().into(), self.decimals)); @@ -88,8 +112,7 @@ impl EthCoin { eth_total_payment, Action::Call(taker_swap_v2_contract), data, - // TODO need new consts and params for v2 calls. now it uses v1 - U256::from(self.gas_limit.eth_payment), + U256::from(self.gas_limit_v2.taker.eth_payment), ) .compat() .await @@ -98,31 +121,14 @@ impl EthCoin { platform: _, token_addr, } => { - let allowed = self - .allowance(taker_swap_v2_contract) - .compat() - .await - .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let data = try_tx_s!(self.prepare_taker_erc20_funding_data(&funding_args, *token_addr).await); - if allowed < payment_amount { - let approved_tx = self.approve(taker_swap_v2_contract, U256::max_value()).compat().await?; - self.wait_for_required_allowance(taker_swap_v2_contract, payment_amount, args.funding_time_lock) - .compat() - .await - .map_err(|e| { - TransactionErr::Plain(ERRL!( - "Allowed value was not updated in time after sending approve transaction {:02x}: {}", - approved_tx.tx_hash_as_bytes(), - e - )) - })?; - } + self.handle_allowance(taker_swap_v2_contract, payment_amount, args.funding_time_lock) + .await?; self.sign_and_send_transaction( U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - // TODO need new consts and params for v2 calls. now it uses v1 - U256::from(self.gas_limit.erc20_payment), + U256::from(self.gas_limit_v2.taker.erc20_payment), ) .compat() .await @@ -144,14 +150,13 @@ impl EthCoin { } let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) .ok_or_else(|| { ValidateSwapV2TxError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string()) - })?; - validate_payment_args(args.taker_secret_hash, args.maker_secret_hash, &args.trading_amount) - .map_err(ValidateSwapV2TxError::Internal)?; - let taker_address = public_to_address(args.taker_pub); + })? + .taker_swap_v2_contract; + let taker_secret_hash = args.taker_secret_hash.try_into()?; + let maker_secret_hash = args.maker_secret_hash.try_into()?; + validate_amount(&args.trading_amount).map_err(ValidateSwapV2TxError::Internal)?; let swap_id = self.etomic_swap_id_v2(args.payment_time_lock, args.maker_secret_hash); let taker_status = self .payment_status_v2( @@ -159,7 +164,7 @@ impl EthCoin { Token::FixedBytes(swap_id.clone()), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await?; @@ -170,6 +175,7 @@ impl EthCoin { args.funding_tx.tx_hash() )) })?; + let taker_address = public_to_address(args.taker_pub); validate_from_to_and_status( tx_from_rpc, taker_address, @@ -186,8 +192,8 @@ impl EthCoin { amount: payment_amount, dex_fee, receiver: self.my_addr().await, - taker_secret_hash: args.taker_secret_hash, - maker_secret_hash: args.maker_secret_hash, + taker_secret_hash, + maker_secret_hash, funding_time_lock: args.funding_time_lock, payment_time_lock: args.payment_time_lock, } @@ -203,7 +209,11 @@ impl EthCoin { let decoded = decode_contract_call(function, &tx_from_rpc.input.0)?; validate_erc20_taker_payment_data(&decoded, &validation_args, function, token_addr)?; }, - EthCoinType::Nft { .. } => unreachable!(), + EthCoinType::Nft { .. } => { + return MmError::err(ValidateSwapV2TxError::ProtocolNotSupported( + "NFT protocol is not supported for ETH and ERC20 Swaps".to_string(), + )); + }, } Ok(()) } @@ -214,9 +224,8 @@ impl EthCoin { &self, args: &GenTakerFundingSpendArgs<'_, Self>, ) -> Result { - // TODO need new consts and params for v2 calls, here should be common `gas_limit.taker_approve` param for Eth and Erc20 let gas_limit = match self.coin_type { - EthCoinType::Eth | EthCoinType::Erc20 { .. } => U256::from(self.gas_limit.eth_payment), + EthCoinType::Eth | EthCoinType::Erc20 { .. } => U256::from(self.gas_limit_v2.taker.approve_payment), EthCoinType::Nft { .. } => { return Err(TransactionErr::ProtocolNotSupported(ERRL!( "NFT protocol is not supported for ETH and ERC20 Swaps" @@ -233,7 +242,7 @@ impl EthCoin { decoded[0].clone(), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await ); @@ -259,34 +268,22 @@ impl EthCoin { &self, args: RefundTakerPaymentArgs<'_>, ) -> Result { - let (token_address, gas_limit) = match &self.coin_type { - // TODO need new consts and params for v2 calls - EthCoinType::Eth => (Address::default(), self.gas_limit.eth_sender_refund), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => (*token_addr, self.gas_limit.erc20_sender_refund), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; - + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; - let dex_fee = try_tx_s!(wei_from_big_decimal( - &args.dex_fee.fee_amount().to_decimal(), - self.decimals - )); - let payment_amount = try_tx_s!(wei_from_big_decimal( - &(args.trading_amount + args.premium_amount), - self.decimals - )); - let maker_address = public_to_address(&Public::from_slice(args.maker_pub)); + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::TakerPayments, + PaymentMethod::RefundTimelock, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let (maker_secret_hash, taker_secret_hash) = match args.tx_type_with_secret_hash { SwapTxTypeWithSecretHash::TakerPaymentV2 { maker_secret_hash, @@ -298,16 +295,26 @@ impl EthCoin { ))) }, }; + let dex_fee = try_tx_s!(wei_from_big_decimal( + &args.dex_fee.fee_amount().to_decimal(), + self.decimals + )); + let payment_amount = try_tx_s!(wei_from_big_decimal( + &(args.trading_amount + args.premium_amount), + self.decimals + )); - let args = TakerRefundArgs { - dex_fee, - payment_amount, - maker_address, - taker_secret: [0u8; 32], - taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), - maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), - payment_time_lock: args.time_lock, - token_address, + let args = { + let maker_address = public_to_address(&Public::from_slice(args.maker_pub)); + TakerRefundTimelockArgs { + dex_fee, + payment_amount, + maker_address, + taker_secret_hash: try_tx_s!(taker_secret_hash.try_into()), + maker_secret_hash: try_tx_s!(maker_secret_hash.try_into()), + payment_time_lock: args.time_lock, + token_address, + } }; let data = try_tx_s!(self.prepare_taker_refund_payment_timelock_data(args).await); @@ -325,25 +332,22 @@ impl EthCoin { &self, args: RefundFundingSecretArgs<'_, Self>, ) -> Result { - let (token_address, gas_limit) = match &self.coin_type { - // TODO need new consts and params for v2 calls - EthCoinType::Eth => (Address::default(), self.gas_limit.eth_sender_refund), - EthCoinType::Erc20 { - platform: _, - token_addr, - } => (*token_addr, self.gas_limit.erc20_sender_refund), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; - + let token_address = self + .get_token_address() + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; let taker_swap_v2_contract = self .swap_v2_contracts - .as_ref() - .map(|contracts| contracts.taker_swap_v2_contract) - .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))?; + .ok_or_else(|| TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")))? + .taker_swap_v2_contract; + let gas_limit = self + .gas_limit_v2 + .gas_limit( + &self.coin_type, + EthPaymentType::TakerPayments, + PaymentMethod::RefundSecret, + ) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let taker_secret = try_tx_s!(args.taker_secret.try_into()); let maker_secret_hash = try_tx_s!(args.maker_secret_hash.try_into()); let dex_fee = try_tx_s!(wei_from_big_decimal( @@ -354,17 +358,18 @@ impl EthCoin { &(args.trading_amount + args.premium_amount), self.decimals )); - let maker_address = public_to_address(args.maker_pubkey); - - let refund_args = TakerRefundArgs { - dex_fee, - payment_amount, - maker_address, - taker_secret, - taker_secret_hash: [0u8; 32], - maker_secret_hash, - payment_time_lock: args.payment_time_lock, - token_address, + + let refund_args = { + let maker_address = public_to_address(args.maker_pubkey); + TakerRefundSecretArgs { + dex_fee, + payment_amount, + maker_address, + taker_secret, + maker_secret_hash, + payment_time_lock: args.payment_time_lock, + token_address, + } }; let data = try_tx_s!(self.prepare_taker_refund_payment_secret_data(&refund_args).await); @@ -394,7 +399,7 @@ impl EthCoin { decoded[0].clone(), // id from takerPaymentApprove &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await .map_err(|e| SearchForFundingSpendErr::Internal(ERRL!("{}", e)))?; @@ -411,16 +416,11 @@ impl EthCoin { gen_args: &GenTakerPaymentSpendArgs<'_, Self>, secret: &[u8], ) -> Result { - // TODO need new consts and params for v2 calls - let gas_limit = match self.coin_type { - EthCoinType::Eth => U256::from(self.gas_limit.eth_receiver_spend), - EthCoinType::Erc20 { .. } => U256::from(self.gas_limit.erc20_receiver_spend), - EthCoinType::Nft { .. } => { - return Err(TransactionErr::ProtocolNotSupported(ERRL!( - "NFT protocol is not supported for ETH and ERC20 Swaps" - ))) - }, - }; + let gas_limit = self + .gas_limit_v2 + .gas_limit(&self.coin_type, EthPaymentType::TakerPayments, PaymentMethod::Spend) + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + let (taker_swap_v2_contract, approve_func, token_address) = self .taker_swap_v2_details(TAKER_PAYMENT_APPROVE, TAKER_PAYMENT_APPROVE) .await?; @@ -431,7 +431,7 @@ impl EthCoin { decoded[0].clone(), &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await ); @@ -450,7 +450,7 @@ impl EthCoin { U256::from(ZERO_VALUE), Action::Call(taker_swap_v2_contract), data, - gas_limit, + U256::from(gas_limit), ) .compat() .await?; @@ -463,7 +463,7 @@ impl EthCoin { &self, taker_payment: &SignedEthTx, wait_until: u64, - ) -> MmResult { + ) -> MmResult { let (decoded, taker_swap_v2_contract) = self .get_decoded_and_swap_contract(taker_payment, "spendTakerPayment") .await?; @@ -474,7 +474,7 @@ impl EthCoin { decoded[0].clone(), // id from spendTakerPayment &TAKER_SWAP_V2, EthPaymentType::TakerPayments, - 3, + TAKER_PAYMENT_STATE_INDEX, ) .await?; if taker_status == U256::from(TakerPaymentStateV2::MakerSpent as u8) { @@ -482,7 +482,7 @@ impl EthCoin { } let now = now_sec(); if now > wait_until { - return MmError::err(WaitForTakerPaymentSpendError::Timeout { wait_until, now }); + return MmError::err(WaitForPaymentSpendError::Timeout { wait_until, now }); } Timer::sleep(10.).await; } @@ -529,7 +529,7 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentTimelock](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L208) method async fn prepare_taker_refund_payment_timelock_data( &self, - args: TakerRefundArgs, + args: TakerRefundTimelockArgs, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentTimelock")?; let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); @@ -548,7 +548,7 @@ impl EthCoin { /// Prepares data for EtomicSwapTakerV2 contract [refundTakerPaymentSecret](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L267) method async fn prepare_taker_refund_payment_secret_data( &self, - args: &TakerRefundArgs, + args: &TakerRefundSecretArgs, ) -> Result, PrepareTxDataError> { let function = TAKER_SWAP_V2.function("refundTakerPaymentSecret")?; let id = self.etomic_swap_id_v2(args.payment_time_lock, &args.maker_secret_hash); @@ -640,38 +640,6 @@ impl EthCoin { Ok(data) } - /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. - pub(crate) async fn payment_status_v2( - &self, - swap_address: Address, - swap_id: Token, - contract_abi: &Contract, - payment_type: EthPaymentType, - state_index: usize, - ) -> Result { - let function_name = payment_type.as_str(); - let function = contract_abi.function(function_name)?; - let data = function.encode_input(&[swap_id])?; - let bytes = self - .call_request(self.my_addr().await, swap_address, None, Some(data.into())) - .await?; - let decoded_tokens = function.decode_output(&bytes.0)?; - - let state = decoded_tokens.get(state_index).ok_or_else(|| { - PaymentStatusErr::Internal(format!( - "Payment status must contain 'state' as the {} token", - state_index - )) - })?; - match state { - Token::Uint(state) => Ok(*state), - _ => Err(PaymentStatusErr::InvalidData(format!( - "Payment status must be Uint, got {:?}", - state - ))), - } - } - /// Retrieves the taker smart contract address, the corresponding function, and the token address. /// /// Depending on the coin type (ETH or ERC20), it fetches the appropriate function name and token address. @@ -726,77 +694,6 @@ impl EthCoin { } } -#[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PrepareTxDataError { - #[from_stringify("ethabi::Error")] - #[display(fmt = "ABI error: {}", _0)] - ABIError(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), -} - -// TODO validate premium when add its support in swap_v2 -fn validate_payment_args<'a>( - taker_secret_hash: &'a [u8], - maker_secret_hash: &'a [u8], - trading_amount: &BigDecimal, -) -> Result<(), String> { - if !is_positive(trading_amount) { - return Err("trading_amount must be a positive value".to_string()); - } - if taker_secret_hash.len() != 32 { - return Err("taker_secret_hash must be 32 bytes".to_string()); - } - if maker_secret_hash.len() != 32 { - return Err("maker_secret_hash must be 32 bytes".to_string()); - } - Ok(()) -} - -/// function to check if BigDecimal is a positive value -#[inline(always)] -fn is_positive(amount: &BigDecimal) -> bool { amount > &BigDecimal::from(0) } - -pub(crate) fn validate_from_to_and_status( - tx_from_rpc: &Web3Tx, - expected_from: Address, - expected_to: Address, - status: U256, - expected_status: u8, -) -> Result<(), MmError> { - if status != U256::from(expected_status) { - return MmError::err(ValidatePaymentV2Err::UnexpectedPaymentState(format!( - "Payment state is not `PaymentSent`, got {}", - status - ))); - } - if tx_from_rpc.from != Some(expected_from) { - return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {:?} was sent from wrong address, expected {:?}", - tx_from_rpc, expected_from - ))); - } - // (in NFT case) as NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address - if tx_from_rpc.to != Some(expected_to) { - return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( - "Payment tx {:?} was sent to wrong address, expected {:?}", - tx_from_rpc, expected_to, - ))); - } - Ok(()) -} - -struct TakerValidationArgs<'a> { - swap_id: Vec, - amount: U256, - dex_fee: U256, - receiver: Address, - taker_secret_hash: &'a [u8], - maker_secret_hash: &'a [u8], - funding_time_lock: u64, - payment_time_lock: u64, -} - /// Validation function for ETH taker payment data fn validate_eth_taker_payment_data( decoded: &[Token], @@ -869,62 +766,3 @@ fn validate_erc20_taker_payment_data( } Ok(()) } - -pub(crate) fn validate_payment_state( - tx: &SignedEthTx, - state: U256, - expected_state: u8, -) -> Result<(), PrepareTxDataError> { - if state != U256::from(expected_state) { - return Err(PrepareTxDataError::Internal(format!( - "Payment {:?} state is not `{}`, got `{}`", - tx, expected_state, state - ))); - } - Ok(()) -} - -#[derive(Debug, Display)] -pub(crate) enum ValidatePaymentV2Err { - UnexpectedPaymentState(String), - WrongPaymentTx(String), -} - -pub(crate) enum EthPaymentType { - MakerPayments, - TakerPayments, -} - -impl EthPaymentType { - pub(crate) fn as_str(&self) -> &'static str { - match self { - EthPaymentType::MakerPayments => "makerPayments", - EthPaymentType::TakerPayments => "takerPayments", - } - } -} - -#[derive(Debug, Display, EnumFromStringify)] -pub(crate) enum PaymentStatusErr { - #[from_stringify("ethabi::Error")] - #[display(fmt = "ABI error: {}", _0)] - ABIError(String), - #[from_stringify("web3::Error")] - #[display(fmt = "Transport error: {}", _0)] - Transport(String), - #[display(fmt = "Internal error: {}", _0)] - Internal(String), - #[display(fmt = "Invalid data error: {}", _0)] - InvalidData(String), -} - -fn check_decoded_length(decoded: &Vec, expected_len: usize) -> Result<(), PrepareTxDataError> { - if decoded.len() != expected_len { - return Err(PrepareTxDataError::Internal(format!( - "Invalid number of tokens in decoded. Expected {}, found {}", - expected_len, - decoded.len() - ))); - } - Ok(()) -} diff --git a/mm2src/coins/eth/eth_swap_v2/mod.rs b/mm2src/coins/eth/eth_swap_v2/mod.rs new file mode 100644 index 0000000000..798a232d56 --- /dev/null +++ b/mm2src/coins/eth/eth_swap_v2/mod.rs @@ -0,0 +1,203 @@ +use crate::eth::{EthCoin, EthCoinType, ParseCoinAssocTypes, Transaction, TransactionErr}; +use enum_derives::EnumFromStringify; +use ethabi::{Contract, Token}; +use ethcore_transaction::SignedTransaction as SignedEthTx; +use ethereum_types::{Address, U256}; +use futures::compat::Future01CompatExt; +use mm2_err_handle::mm_error::MmError; +use mm2_number::BigDecimal; +use num_traits::Signed; +use web3::types::Transaction as Web3Tx; + +pub(crate) mod eth_maker_swap_v2; +pub(crate) mod eth_taker_swap_v2; + +/// ZERO_VALUE is used to represent a 0 amount in transactions where the value is encoded in the transaction input data. +/// This is typically used in function calls where the value is not directly transferred with the transaction, such as in +/// `spendTakerPayment` where the [amount](https://github.com/KomodoPlatform/etomic-swap/blob/5e15641cbf41766cd5b37b4d71842c270773f788/contracts/EtomicSwapTakerV2.sol#L166) +/// is provided as part of the input data rather than as an Ether value +pub(crate) const ZERO_VALUE: u32 = 0; + +pub enum EthPaymentType { + MakerPayments, + TakerPayments, +} + +impl EthPaymentType { + pub(crate) fn as_str(&self) -> &'static str { + match self { + EthPaymentType::MakerPayments => "makerPayments", + EthPaymentType::TakerPayments => "takerPayments", + } + } +} + +pub enum PaymentMethod { + Send, + Spend, + RefundTimelock, + RefundSecret, +} + +#[derive(Debug, Display)] +pub(crate) enum ValidatePaymentV2Err { + UnexpectedPaymentState(String), + WrongPaymentTx(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PaymentStatusErr { + #[from_stringify("ethabi::Error")] + #[display(fmt = "ABI error: {}", _0)] + ABIError(String), + #[from_stringify("web3::Error")] + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), + #[display(fmt = "Invalid data error: {}", _0)] + InvalidData(String), +} + +#[derive(Debug, Display, EnumFromStringify)] +pub(crate) enum PrepareTxDataError { + #[from_stringify("ethabi::Error")] + #[display(fmt = "ABI error: {}", _0)] + ABIError(String), + #[display(fmt = "Internal error: {}", _0)] + Internal(String), +} + +impl EthCoin { + /// Retrieves the payment status from a given smart contract address based on the swap ID and state type. + pub(crate) async fn payment_status_v2( + &self, + swap_address: Address, + swap_id: Token, + contract_abi: &Contract, + payment_type: EthPaymentType, + state_index: usize, + ) -> Result { + let function_name = payment_type.as_str(); + let function = contract_abi.function(function_name)?; + let data = function.encode_input(&[swap_id])?; + let bytes = self + .call_request(self.my_addr().await, swap_address, None, Some(data.into())) + .await?; + let decoded_tokens = function.decode_output(&bytes.0)?; + + let state = decoded_tokens.get(state_index).ok_or_else(|| { + PaymentStatusErr::Internal(format!( + "Payment status must contain 'state' as the {} token", + state_index + )) + })?; + match state { + Token::Uint(state) => Ok(*state), + _ => Err(PaymentStatusErr::InvalidData(format!( + "Payment status must be Uint, got {:?}", + state + ))), + } + } + + pub(super) fn get_token_address(&self) -> Result { + match &self.coin_type { + EthCoinType::Eth => Ok(Address::default()), + EthCoinType::Erc20 { token_addr, .. } => Ok(*token_addr), + EthCoinType::Nft { .. } => Err("NFT protocol is not supported for ETH and ERC20 Swaps".to_string()), + } + } +} + +pub(crate) fn validate_payment_state( + tx: &SignedEthTx, + state: U256, + expected_state: u8, +) -> Result<(), PrepareTxDataError> { + if state != U256::from(expected_state) { + return Err(PrepareTxDataError::Internal(format!( + "Payment {:?} state is not `{}`, got `{}`", + tx, expected_state, state + ))); + } + Ok(()) +} + +pub(crate) fn validate_from_to_and_status( + tx_from_rpc: &Web3Tx, + expected_from: Address, + expected_to: Address, + status: U256, + expected_status: u8, +) -> Result<(), MmError> { + if status != U256::from(expected_status) { + return MmError::err(ValidatePaymentV2Err::UnexpectedPaymentState(format!( + "Payment state is not `PaymentSent`, got {}", + status + ))); + } + if tx_from_rpc.from != Some(expected_from) { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( + "Payment tx {:?} was sent from wrong address, expected {:?}", + tx_from_rpc, expected_from + ))); + } + // (in NFT case) as NFT owner calls "safeTransferFrom" directly, then in Transaction 'to' field we expect token_address + if tx_from_rpc.to != Some(expected_to) { + return MmError::err(ValidatePaymentV2Err::WrongPaymentTx(format!( + "Payment tx {:?} was sent to wrong address, expected {:?}", + tx_from_rpc, expected_to, + ))); + } + Ok(()) +} + +// TODO validate premium when add its support in swap_v2 +fn validate_amount(trading_amount: &BigDecimal) -> Result<(), String> { + if !trading_amount.is_positive() { + return Err("trading_amount must be a positive value".to_string()); + } + Ok(()) +} + +fn check_decoded_length(decoded: &Vec, expected_len: usize) -> Result<(), PrepareTxDataError> { + if decoded.len() != expected_len { + return Err(PrepareTxDataError::Internal(format!( + "Invalid number of tokens in decoded. Expected {}, found {}", + expected_len, + decoded.len() + ))); + } + Ok(()) +} + +impl EthCoin { + async fn handle_allowance( + &self, + swap_contract: Address, + payment_amount: U256, + time_lock: u64, + ) -> Result<(), TransactionErr> { + let allowed = self + .allowance(swap_contract) + .compat() + .await + .map_err(|e| TransactionErr::Plain(ERRL!("{}", e)))?; + + if allowed < payment_amount { + let approved_tx = self.approve(swap_contract, U256::max_value()).compat().await?; + self.wait_for_required_allowance(swap_contract, payment_amount, time_lock) + .compat() + .await + .map_err(|e| { + TransactionErr::Plain(ERRL!( + "Allowed value was not updated in time after sending approve transaction {:02x}: {}", + approved_tx.tx_hash_as_bytes(), + e + )) + })?; + } + Ok(()) + } +} diff --git a/mm2src/coins/eth/eth_tests.rs b/mm2src/coins/eth/eth_tests.rs index 3a534b603a..d01ae6d2aa 100644 --- a/mm2src/coins/eth/eth_tests.rs +++ b/mm2src/coins/eth/eth_tests.rs @@ -1,13 +1,13 @@ use super::*; use crate::IguanaPrivKey; -use common::{block_on, block_on_f01}; +use common::block_on; use mm2_core::mm_ctx::MmCtxBuilder; cfg_native!( use crate::eth::for_tests::{eth_coin_for_test, eth_coin_from_keypair}; use crate::DexFee; - use common::now_sec; + use common::{now_sec, block_on_f01}; use ethkey::{Generator, Random}; use mm2_test_helpers::for_tests::{ETH_MAINNET_CHAIN_ID, ETH_MAINNET_NODE, ETH_SEPOLIA_CHAIN_ID, ETH_SEPOLIA_NODES, ETH_SEPOLIA_TOKEN_CONTRACT}; @@ -191,7 +191,7 @@ fn test_wait_for_payment_spend_timeout() { 184, 42, 106, ]; - assert!(block_on_f01(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + assert!(block_on(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &tx_bytes, secret_hash: &[], wait_until, @@ -1036,3 +1036,10 @@ fn test_gas_limit_conf() { && eth_coin.gas_limit.eth_max_trade_gas == 150_000 ); } + +#[test] +fn test_h256_to_str() { + let h = H256::from_str("5136701f11060010841c9708c3eb26f6606a070b8ae43f4b98b6d7b10a545258").unwrap(); + let b: BytesJson = h.0.to_vec().into(); + println!("H256=0x{:02x}", b); +} diff --git a/mm2src/coins/eth/for_tests.rs b/mm2src/coins/eth/for_tests.rs index d3b8ece3ac..cc6d5cd375 100644 --- a/mm2src/coins/eth/for_tests.rs +++ b/mm2src/coins/eth/for_tests.rs @@ -50,7 +50,8 @@ pub(crate) fn eth_coin_from_keypair( }; let my_address = key_pair.address(); let coin_conf = coin_conf(&ctx, &ticker); - let gas_limit = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&coin_conf).expect("expected valid gas_limit config"); let eth_coin = EthCoin(Arc::new(EthCoinImpl { coin_type, @@ -77,6 +78,7 @@ pub(crate) fn eth_coin_from_keypair( nfts_infos: Arc::new(Default::default()), platform_fee_estimator_state: Arc::new(FeeEstimatorState::CoinNotSupported), gas_limit, + gas_limit_v2, abortable_system: AbortableQueue::default(), })); (ctx, eth_coin) diff --git a/mm2src/coins/eth/nft_swap_v2/mod.rs b/mm2src/coins/eth/nft_swap_v2/mod.rs index d5de6dcc86..f4c909bd32 100644 --- a/mm2src/coins/eth/nft_swap_v2/mod.rs +++ b/mm2src/coins/eth/nft_swap_v2/mod.rs @@ -1,4 +1,3 @@ -use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; use ethabi::{Contract, Token}; use ethcore_transaction::Action; use ethereum_types::{Address, U256}; @@ -6,21 +5,23 @@ use ethkey::public_to_address; use futures::compat::Future01CompatExt; use mm2_err_handle::prelude::{MapToMmResult, MmError, MmResult}; use mm2_number::BigDecimal; +use num_traits::Signed; use web3::types::TransactionId; -pub(crate) mod errors; -use errors::{Erc721FunctionError, HtlcParamsError, PrepareTxDataError}; -mod structs; -use structs::{ExpectedHtlcParams, ValidationParams}; - use super::ContractType; -use crate::eth::eth_swap_v2::{validate_from_to_and_status, validate_payment_state, EthPaymentType, PaymentStatusErr, - ZERO_VALUE}; +use crate::coin_errors::{ValidatePaymentError, ValidatePaymentResult}; +use crate::eth::eth_swap_v2::{validate_from_to_and_status, validate_payment_state, EthPaymentType, PaymentMethod, + PaymentStatusErr, PrepareTxDataError, ZERO_VALUE}; use crate::eth::{decode_contract_call, EthCoin, EthCoinType, MakerPaymentStateV2, SignedEthTx, ERC1155_CONTRACT, ERC721_CONTRACT, NFT_MAKER_SWAP_V2}; use crate::{ParseCoinAssocTypes, RefundNftMakerPaymentArgs, SendNftMakerPaymentArgs, SpendNftMakerPaymentArgs, TransactionErr, ValidateNftMakerPaymentArgs}; +pub(crate) mod errors; +use errors::{Erc721FunctionError, HtlcParamsError}; +mod structs; +use structs::{ExpectedHtlcParams, ValidationParams}; + impl EthCoin { pub(crate) async fn send_nft_maker_payment_v2_impl( &self, @@ -37,11 +38,14 @@ impl EthCoin { let htlc_data = try_tx_s!(self.prepare_htlc_data(&args)); let data = try_tx_s!(self.prepare_nft_maker_payment_v2_data(&args, htlc_data).await); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.nft_swap_info.contract_type, PaymentMethod::Send); self.sign_and_send_transaction( ZERO_VALUE.into(), Action::Call(*args.nft_swap_info.token_address), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -58,6 +62,14 @@ impl EthCoin { ) -> ValidatePaymentResult<()> { match self.coin_type { EthCoinType::Nft { .. } => { + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + ValidatePaymentError::InternalError( + "Expected swap_v2_contracts to be Some, but found None".to_string(), + ) + })? + .nft_maker_swap_v2_contract; let contract_type = args.nft_swap_info.contract_type; validate_payment_args( args.taker_secret_hash, @@ -66,14 +78,12 @@ impl EthCoin { contract_type, ) .map_err(ValidatePaymentError::InternalError)?; - // TODO use swap contract address from self - let etomic_swap_contract = args.nft_swap_info.swap_contract_address; let token_address = args.nft_swap_info.token_address; let maker_address = public_to_address(args.maker_pub); let swap_id = self.etomic_swap_id_v2(args.time_lock, args.maker_secret_hash); let maker_status = self .payment_status_v2( - *etomic_swap_contract, + nft_maker_swap_v2_contract, Token::FixedBytes(swap_id.clone()), &NFT_MAKER_SWAP_V2, EthPaymentType::MakerPayments, @@ -107,7 +117,7 @@ impl EthCoin { let validation_params = ValidationParams { maker_address, - etomic_swap_contract: *etomic_swap_contract, + nft_maker_swap_v2_contract, token_id: args.nft_swap_info.token_id, amount, }; @@ -139,7 +149,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; if args.maker_secret.len() != 32 { return Err(TransactionErr::Plain(ERRL!("maker_secret must be 32 bytes"))); } @@ -150,7 +165,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -160,11 +175,14 @@ impl EthCoin { .await ); let data = try_tx_s!(self.prepare_spend_nft_maker_v2_data(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::Spend); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -181,7 +199,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; let (decoded, bytes_index) = try_tx_s!(get_decoded_tx_data_and_bytes_index( args.contract_type, args.maker_payment_tx.unsigned().data() @@ -189,7 +212,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -200,11 +223,14 @@ impl EthCoin { ); let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_timelock(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::RefundTimelock); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -221,7 +247,12 @@ impl EthCoin { ) -> Result { match self.coin_type { EthCoinType::Nft { .. } => { - let etomic_swap_contract = args.swap_contract_address; + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + TransactionErr::Plain(ERRL!("Expected swap_v2_contracts to be Some, but found None")) + })? + .nft_maker_swap_v2_contract; let (decoded, bytes_index) = try_tx_s!(get_decoded_tx_data_and_bytes_index( args.contract_type, args.maker_payment_tx.unsigned().data() @@ -229,7 +260,7 @@ impl EthCoin { let (state, htlc_params) = try_tx_s!( self.status_and_htlc_params_from_tx_data( - *etomic_swap_contract, + nft_maker_swap_v2_contract, &NFT_MAKER_SWAP_V2, &decoded, bytes_index, @@ -241,11 +272,14 @@ impl EthCoin { let data = try_tx_s!(self.prepare_refund_nft_maker_payment_v2_secret(&args, decoded, htlc_params, state)); + let gas_limit = self + .gas_limit_v2 + .nft_gas_limit(args.contract_type, PaymentMethod::RefundSecret); self.sign_and_send_transaction( ZERO_VALUE.into(), - Action::Call(*etomic_swap_contract), + Action::Call(nft_maker_swap_v2_contract), data, - U256::from(self.gas_limit.eth_max_trade_gas), // TODO: fix to a more accurate const or estimated value + U256::from(gas_limit), ) .compat() .await @@ -261,6 +295,12 @@ impl EthCoin { args: &SendNftMakerPaymentArgs<'_, Self>, htlc_data: Vec, ) -> Result, PrepareTxDataError> { + let nft_maker_swap_v2_contract = self + .swap_v2_contracts + .ok_or_else(|| { + PrepareTxDataError::Internal("Expected swap_v2_contracts to be Some, but found None".to_string()) + })? + .nft_maker_swap_v2_contract; match args.nft_swap_info.contract_type { ContractType::Erc1155 => { let function = ERC1155_CONTRACT.function("safeTransferFrom")?; @@ -268,7 +308,7 @@ impl EthCoin { .map_err(|e| PrepareTxDataError::Internal(e.to_string()))?; let data = function.encode_input(&[ Token::Address(self.my_addr().await), - Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Address(nft_maker_swap_v2_contract), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Uint(amount_u256), Token::Bytes(htlc_data), @@ -279,7 +319,7 @@ impl EthCoin { let function = erc721_transfer_with_data()?; let data = function.encode_input(&[ Token::Address(self.my_addr().await), - Token::Address(*args.nft_swap_info.swap_contract_address), + Token::Address(nft_maker_swap_v2_contract), Token::Uint(U256::from(args.nft_swap_info.token_id)), Token::Bytes(htlc_data), ])?; @@ -438,7 +478,11 @@ impl EthCoin { fn validate_decoded_data(decoded: &[Token], params: &ValidationParams) -> Result<(), MmError> { let checks = vec![ (0, Token::Address(params.maker_address), "maker_address"), - (1, Token::Address(params.etomic_swap_contract), "etomic_swap_contract"), + ( + 1, + Token::Address(params.nft_maker_swap_v2_contract), + "nft_maker_swap_v2_contract", + ), (2, Token::Uint(U256::from(params.token_id)), "token_id"), ]; @@ -528,7 +572,7 @@ fn htlc_params() -> &'static [ethabi::ParamType] { /// function to check if BigDecimal is a positive integer #[inline(always)] -fn is_positive_integer(amount: &BigDecimal) -> bool { amount == &amount.with_scale(0) && amount > &BigDecimal::from(0) } +fn is_positive_integer(amount: &BigDecimal) -> bool { amount == &amount.with_scale(0) && amount.is_positive() } fn validate_payment_args<'a>( taker_secret_hash: &'a [u8], diff --git a/mm2src/coins/eth/nft_swap_v2/structs.rs b/mm2src/coins/eth/nft_swap_v2/structs.rs index 2866d81a01..7bd4130d9f 100644 --- a/mm2src/coins/eth/nft_swap_v2/structs.rs +++ b/mm2src/coins/eth/nft_swap_v2/structs.rs @@ -11,7 +11,7 @@ pub(crate) struct ExpectedHtlcParams { pub(crate) struct ValidationParams<'a> { pub(crate) maker_address: Address, - pub(crate) etomic_swap_contract: Address, + pub(crate) nft_maker_swap_v2_contract: Address, pub(crate) token_id: &'a [u8], // Optional, as it's not needed for ERC721 pub(crate) amount: Option, diff --git a/mm2src/coins/eth/v2_activation.rs b/mm2src/coins/eth/v2_activation.rs index 576920b030..cee2313ba2 100644 --- a/mm2src/coins/eth/v2_activation.rs +++ b/mm2src/coins/eth/v2_activation.rs @@ -1,4 +1,5 @@ use super::*; +use crate::eth::erc20::{get_enabled_erc20_by_contract, get_token_decimals}; use crate::eth::web3_transport::http_transport::HttpTransport; use crate::hd_wallet::{load_hd_accounts_from_storage, HDAccountsMutex, HDPathAccountToAddressId, HDWalletCoinStorage, HDWalletStorageError, DEFAULT_GAP_LIMIT}; @@ -62,6 +63,8 @@ pub enum EthActivationV2Error { HwError(HwRpcError), #[display(fmt = "Hardware wallet must be called within rpc task framework")] InvalidHardwareWalletCall, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EthActivationV2Error { @@ -93,6 +96,7 @@ impl From for EthActivationV2Error { EthActivationV2Error::UnexpectedDerivationMethod(err) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EthActivationV2Error::PrivKeyPolicyNotAllowed(e), + EthTokenActivationError::CustomTokenError(e) => EthActivationV2Error::CustomTokenError(e), } } } @@ -211,6 +215,7 @@ pub enum EthTokenActivationError { Transport(String), UnexpectedDerivationMethod(UnexpectedDerivationMethod), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + CustomTokenError(CustomTokenError), } impl From for EthTokenActivationError { @@ -376,9 +381,11 @@ pub struct NftProtocol { impl EthCoin { pub async fn initialize_erc20_token( &self, + ticker: String, activation_params: Erc20TokenActivationRequest, + token_conf: Json, protocol: Erc20Protocol, - ticker: String, + is_custom: bool, ) -> MmResult { // TODO // Check if ctx is required. @@ -387,9 +394,24 @@ impl EthCoin { .ok_or_else(|| String::from("No context")) .map_err(EthTokenActivationError::InternalError)?; - let conf = coin_conf(&ctx, &ticker); + // Todo: when custom token config storage is added, this might not be needed + // `is_custom` was added to avoid this unnecessary check for non-custom tokens + if is_custom { + match get_enabled_erc20_by_contract(&ctx, protocol.token_addr).await { + Ok(Some(token)) => { + return MmError::err(EthTokenActivationError::CustomTokenError( + CustomTokenError::TokenWithSameContractAlreadyActivated { + ticker: token.ticker().to_string(), + contract_address: display_eth_address(&protocol.token_addr), + }, + )); + }, + Ok(None) => {}, + Err(e) => return MmError::err(EthTokenActivationError::InternalError(e.to_string())), + } + } - let decimals = match conf["decimals"].as_u64() { + let decimals = match token_conf["decimals"].as_u64() { None | Some(0) => get_token_decimals( &self .web3() @@ -404,7 +426,11 @@ impl EthCoin { let required_confirmations = activation_params .required_confirmations - .unwrap_or_else(|| conf["required_confirmations"].as_u64().unwrap_or(1)) + .unwrap_or_else(|| { + token_conf["required_confirmations"] + .as_u64() + .unwrap_or(self.required_confirmations()) + }) .into(); // Create an abortable system linked to the `MmCtx` so if the app is stopped on `MmArc::stop`, @@ -415,9 +441,11 @@ impl EthCoin { platform: protocol.platform, token_addr: protocol.token_addr, }; - let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; - let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(&conf) + let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &token_conf, &coin_type).await?; + let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &token_conf, &coin_type).await?; + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&token_conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&token_conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let token = EthCoinImpl { @@ -448,6 +476,7 @@ impl EthCoin { nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; @@ -506,7 +535,9 @@ impl EthCoin { }; let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(&ctx, &conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(&ctx, &conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(&conf) + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(&conf) + .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(&conf) .map_to_mm(|e| EthTokenActivationError::InternalError(format!("invalid gas_limit config {}", e)))?; let global_nft = EthCoinImpl { @@ -534,6 +565,7 @@ impl EthCoin { nfts_infos: Arc::new(AsyncMutex::new(nft_infos)), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; Ok(EthCoin(Arc::new(global_nft))) @@ -639,7 +671,9 @@ pub async fn eth_coin_from_conf_and_request_v2( let coin_type = EthCoinType::Eth; let platform_fee_estimator_state = FeeEstimatorState::init_fee_estimator(ctx, conf, &coin_type).await?; let max_eth_tx_type = get_max_eth_tx_type_conf(ctx, conf, &coin_type).await?; - let gas_limit = extract_gas_limit_from_conf(conf) + let gas_limit: EthGasLimit = extract_gas_limit_from_conf(conf) + .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; + let gas_limit_v2: EthGasLimitV2 = extract_gas_limit_from_conf(conf) .map_to_mm(|e| EthActivationV2Error::InternalError(format!("invalid gas_limit config {}", e)))?; let coin = EthCoinImpl { @@ -667,6 +701,7 @@ pub async fn eth_coin_from_conf_and_request_v2( nfts_infos: Default::default(), platform_fee_estimator_state, gas_limit, + gas_limit_v2, abortable_system, }; diff --git a/mm2src/coins/eth/web3_transport/websocket_transport.rs b/mm2src/coins/eth/web3_transport/websocket_transport.rs index 6d19573781..36b13bdb78 100644 --- a/mm2src/coins/eth/web3_transport/websocket_transport.rs +++ b/mm2src/coins/eth/web3_transport/websocket_transport.rs @@ -52,8 +52,8 @@ pub struct WebsocketTransport { #[derive(Debug)] struct ControllerChannel { - tx: Arc>>, - rx: Arc>>, + tx: UnboundedSender, + rx: AsyncMutex>, } enum ControllerMessage { @@ -86,11 +86,10 @@ impl WebsocketTransport { node, event_handlers, request_id: Arc::new(AtomicUsize::new(1)), - controller_channel: ControllerChannel { - tx: Arc::new(AsyncMutex::new(req_tx)), - rx: Arc::new(AsyncMutex::new(req_rx)), - } - .into(), + controller_channel: Arc::new(ControllerChannel { + tx: req_tx, + rx: AsyncMutex::new(req_rx), + }), connection_guard: Arc::new(AsyncMutex::new(())), proxy_sign_keypair: None, last_request_failed: Arc::new(AtomicBool::new(false)), @@ -298,7 +297,7 @@ impl WebsocketTransport { } pub(crate) async fn stop_connection_loop(&self) { - let mut tx = self.controller_channel.tx.lock().await; + let mut tx = self.controller_channel.tx.clone(); tx.send(ControllerMessage::Close) .await .expect("receiver channel must be alive"); @@ -357,12 +356,11 @@ async fn send_request( serialized_request = serde_json::to_string(&wrapper)?; } - let mut tx = transport.controller_channel.tx.lock().await; - let (notification_sender, notification_receiver) = oneshot::channel::>(); event_handlers.on_outgoing_request(&request_bytes); + let mut tx = transport.controller_channel.tx.clone(); tx.send(ControllerMessage::Request(WsRequest { request_id, serialized_request, diff --git a/mm2src/coins/hd_wallet/storage/sqlite_storage.rs b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs index 898f4c8823..f430eac042 100644 --- a/mm2src/coins/hd_wallet/storage/sqlite_storage.rs +++ b/mm2src/coins/hd_wallet/storage/sqlite_storage.rs @@ -101,7 +101,7 @@ impl HDWalletStorageInternalOps for HDWalletSqliteStorage { where Self: Sized, { - let shared = ctx.shared_sqlite_conn.as_option().or_mm_err(|| { + let shared = ctx.shared_sqlite_conn.get().or_mm_err(|| { HDWalletStorageError::Internal("'MmCtx::shared_sqlite_conn' is not initialized".to_owned()) })?; let storage = HDWalletSqliteStorage { diff --git a/mm2src/coins/lightning.rs b/mm2src/coins/lightning.rs index f3b364e0ef..682eed2eb9 100644 --- a/mm2src/coins/lightning.rs +++ b/mm2src/coins/lightning.rs @@ -1158,58 +1158,53 @@ impl MarketCoinOps for LightningCoin { Box::new(fut.boxed().compat()) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let payment_hash = try_tx_fus!(payment_hash_from_slice(args.tx_bytes)); + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + let payment_hash = try_tx_s!(payment_hash_from_slice(args.tx_bytes)); let payment_hex = hex::encode(payment_hash.0); - let coin = self.clone(); - let wait_until = args.wait_until; - let fut = async move { - loop { - if now_sec() > wait_until { - return Err(TransactionErr::Plain(ERRL!( - "Waited too long until {} for payment {} to be spent", - wait_until, - payment_hex - ))); - } + loop { + if now_sec() > args.wait_until { + return Err(TransactionErr::Plain(ERRL!( + "Waited too long until {} for payment {} to be spent", + args.wait_until, + payment_hex + ))); + } - match coin.db.get_payment_from_db(payment_hash).await { - Ok(Some(payment)) => match payment.status { - HTLCStatus::Pending => (), - HTLCStatus::Claimable => { - return Err(TransactionErr::Plain(ERRL!( - "Payment {} has an invalid status of {} in the db", - payment_hex, - payment.status - ))) - }, - HTLCStatus::Succeeded => return Ok(TransactionEnum::LightningPayment(payment_hash)), - HTLCStatus::Failed => { - return Err(TransactionErr::Plain(ERRL!( - "Lightning swap payment {} failed", - payment_hex - ))) - }, - }, - Ok(None) => return Err(TransactionErr::Plain(ERRL!("Payment {} not found in DB", payment_hex))), - Err(e) => { + match self.db.get_payment_from_db(payment_hash).await { + Ok(Some(payment)) => match payment.status { + HTLCStatus::Pending => (), + HTLCStatus::Claimable => { return Err(TransactionErr::Plain(ERRL!( - "Error getting payment {} from db: {}", + "Payment {} has an invalid status of {} in the db", payment_hex, - e + payment.status ))) }, - } - - // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when sleeping for 10 seconds - // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together. - // Todo: The aim is to make lightning swap payments as fast as possible, more sleep time can be allowed for maker payment since it waits for the secret to be revealed on another chain first. - // Todo: Running swap payments statuses should be loaded from db on restarts in this case. - Timer::sleep(10.).await; + HTLCStatus::Succeeded => return Ok(TransactionEnum::LightningPayment(payment_hash)), + HTLCStatus::Failed => { + return Err(TransactionErr::Plain(ERRL!( + "Lightning swap payment {} failed", + payment_hex + ))) + }, + }, + Ok(None) => return Err(TransactionErr::Plain(ERRL!("Payment {} not found in DB", payment_hex))), + Err(e) => { + return Err(TransactionErr::Plain(ERRL!( + "Error getting payment {} from db: {}", + payment_hex, + e + ))) + }, } - }; - Box::new(fut.boxed().compat()) + + // note: When sleeping for only 1 second the test_send_payment_and_swaps unit test took 20 seconds to complete instead of 37 seconds when sleeping for 10 seconds + // Todo: In next sprints, should add a mutex for lightning swap payments to avoid overloading the shared db connection with requests when the sleep time is reduced and multiple swaps are ran together. + // Todo: The aim is to make lightning swap payments as fast as possible, more sleep time can be allowed for maker payment since it waits for the secret to be revealed on another chain first. + // Todo: Running swap payments statuses should be loaded from db on restarts in this case. + Timer::sleep(10.).await; + } } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/lightning/ln_platform.rs b/mm2src/coins/lightning/ln_platform.rs index 59e3e19488..a3a4c22776 100644 --- a/mm2src/coins/lightning/ln_platform.rs +++ b/mm2src/coins/lightning/ln_platform.rs @@ -542,7 +542,6 @@ impl Platform { check_every: TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, watcher_reward: false, }) - .compat() .await .map_to_mm(|e| SaveChannelClosingError::WaitForFundingTxSpendError(e.get_plain_text_format()))?; diff --git a/mm2src/coins/lightning/ln_utils.rs b/mm2src/coins/lightning/ln_utils.rs index 5b4ac5698d..79868908fa 100644 --- a/mm2src/coins/lightning/ln_utils.rs +++ b/mm2src/coins/lightning/ln_utils.rs @@ -87,6 +87,7 @@ pub async fn init_db(ctx: &MmArc, ticker: String) -> EnableLightningResult std::fmt::Result { write!(f, "{}", self.get_plain_text_format()) } +} + #[derive(Debug, PartialEq)] pub enum FoundSwapTxSpend { Spent(TransactionEnum), @@ -962,9 +966,9 @@ pub struct RefundMakerPaymentTimelockArgs<'a> { pub time_lock: u64, pub taker_pub: &'a [u8], pub tx_type_with_secret_hash: SwapTxTypeWithSecretHash<'a>, - pub swap_contract_address: &'a Option, pub swap_unique_data: &'a [u8], pub watcher_reward: bool, + pub amount: BigDecimal, } #[derive(Debug)] @@ -1463,7 +1467,7 @@ pub enum ValidateSwapV2TxError { /// Indicates that overflow occurred, either while calculating a total payment or converting the timelock. Overflow(String), /// Internal error - #[from_stringify("ethabi::Error")] + #[from_stringify("ethabi::Error", "TryFromSliceError")] Internal(String), /// Payment transaction is in unexpected state. E.g., `Uninitialized` instead of `PaymentSent` for ETH payment. UnexpectedPaymentState(String), @@ -1629,8 +1633,6 @@ pub struct NftSwapInfo<'a, Coin: ParseNftAssocTypes + ?Sized> { pub token_id: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } pub struct SendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { @@ -1703,6 +1705,7 @@ pub struct RefundMakerPaymentSecretArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> pub taker_pub: &'a Coin::Pubkey, /// Unique data of specific swap pub swap_unique_data: &'a [u8], + pub amount: BigDecimal, } /// Common refund NFT Maker Payment structure for [MakerNftSwapOpsV2::refund_nft_maker_payment_v2_timelock] and @@ -1720,8 +1723,6 @@ pub struct RefundNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAss pub swap_unique_data: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { @@ -1739,6 +1740,7 @@ pub struct SpendMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ?Sized> { pub maker_pub: &'a Coin::Pubkey, /// Unique data of specific swap pub swap_unique_data: &'a [u8], + pub amount: BigDecimal, } pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAssocTypes + ?Sized> { @@ -1756,8 +1758,6 @@ pub struct SpendNftMakerPaymentArgs<'a, Coin: ParseCoinAssocTypes + ParseNftAsso pub swap_unique_data: &'a [u8], /// The type of smart contract that governs this NFT pub contract_type: &'a Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: &'a Coin::ContractAddress, } /// Operations specific to maker coin in [Trading Protocol Upgrade implementation](https://github.com/KomodoPlatform/komodo-defi-framework/issues/1895) @@ -1819,7 +1819,7 @@ pub trait MakerNftSwapOpsV2: ParseCoinAssocTypes + ParseNftAssocTypes + Send + S /// Enum representing errors that can occur while waiting for taker payment spend. #[derive(Display, Debug, EnumFromStringify)] -pub enum WaitForTakerPaymentSpendError { +pub enum WaitForPaymentSpendError { /// Timeout error variant, indicating that the wait for taker payment spend has timed out. #[display( fmt = "Timed out waiting for taker payment spend, wait_until {}, now {}", @@ -1842,20 +1842,18 @@ pub enum WaitForTakerPaymentSpendError { Transport(String), } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(err: WaitForOutputSpendErr) -> Self { match err { - WaitForOutputSpendErr::Timeout { wait_until, now } => { - WaitForTakerPaymentSpendError::Timeout { wait_until, now } - }, + WaitForOutputSpendErr::Timeout { wait_until, now } => WaitForPaymentSpendError::Timeout { wait_until, now }, WaitForOutputSpendErr::NoOutputWithIndex(index) => { - WaitForTakerPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) + WaitForPaymentSpendError::InvalidInputTx(format!("Tx doesn't have output with index {}", index)) }, } } } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(e: PaymentStatusErr) -> Self { match e { PaymentStatusErr::ABIError(e) => Self::ABIError(e), @@ -1866,7 +1864,7 @@ impl From for WaitForTakerPaymentSpendError { } } -impl From for WaitForTakerPaymentSpendError { +impl From for WaitForPaymentSpendError { fn from(e: PrepareTxDataError) -> Self { match e { PrepareTxDataError::ABIError(e) => Self::ABIError(e), @@ -2007,7 +2005,7 @@ pub trait TakerCoinSwapOpsV2: ParseCoinAssocTypes + CommonSwapOpsV2 + Send + Syn taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult; + ) -> MmResult; } #[async_trait] @@ -2066,7 +2064,13 @@ pub trait MarketCoinOps { fn wait_for_confirmations(&self, input: ConfirmPaymentInput) -> Box + Send>; - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut; + /// Waits for spending/unlocking of funds locked in a HTLC construction specific to the coin's + /// chain. Implementation should monitor locked funds (UTXO/contract/etc.) until funds are + /// spent/unlocked or timeout is reached. + /// + /// Returns spending tx/event from mempool/pending state to allow prompt extraction of preimage + /// secret. + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult; fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result>; @@ -2682,7 +2686,7 @@ pub enum BalanceError { UnexpectedDerivationMethod(UnexpectedDerivationMethod), #[display(fmt = "Wallet storage error: {}", _0)] WalletStorageError(String), - #[from_stringify("Bip32Error", "NumConversError")] + #[from_stringify("Bip32Error", "NumConversError", "ParseBigIntError")] #[display(fmt = "Internal: {}", _0)] Internal(String), } @@ -3034,8 +3038,8 @@ pub enum WithdrawError { NotEnoughNftsAmount { token_address: String, token_id: String, - available: BigDecimal, - required: BigDecimal, + available: BigUint, + required: BigUint, }, #[display(fmt = "DB error {}", _0)] DbError(String), @@ -3316,6 +3320,10 @@ pub trait MmCoin: /// The coin can be initialized, but it cannot participate in the swaps. fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } coin_conf["wallet_only"].as_bool().unwrap_or(false) } @@ -4453,6 +4461,97 @@ pub enum CoinProtocol { }, } +#[derive(Clone, Debug, Deserialize, Display, PartialEq, Serialize)] +pub enum CustomTokenError { + #[display( + fmt = "Token with the same ticker already exists in coins configs, ticker in config: {}", + ticker_in_config + )] + DuplicateTickerInConfig { ticker_in_config: String }, + #[display( + fmt = "Token with the same contract address already exists in coins configs, ticker in config: {}", + ticker_in_config + )] + DuplicateContractInConfig { ticker_in_config: String }, + #[display( + fmt = "Token is already activated, ticker: {}, contract address: {}", + ticker, + contract_address + )] + TokenWithSameContractAlreadyActivated { ticker: String, contract_address: String }, +} + +impl CoinProtocol { + /// Returns the platform coin associated with the coin protocol, if any. + pub fn platform(&self) -> Option<&str> { + match self { + CoinProtocol::QRC20 { platform, .. } + | CoinProtocol::ERC20 { platform, .. } + | CoinProtocol::SLPTOKEN { platform, .. } + | CoinProtocol::NFT { platform, .. } => Some(platform), + CoinProtocol::TENDERMINTTOKEN(info) => Some(&info.platform), + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { platform, .. } => Some(platform), + CoinProtocol::UTXO + | CoinProtocol::QTUM + | CoinProtocol::ETH + | CoinProtocol::BCH { .. } + | CoinProtocol::TENDERMINT(_) + | CoinProtocol::ZHTLC(_) => None, + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA => None, + } + } + + /// Returns the contract address associated with the coin, if any. + pub fn contract_address(&self) -> Option<&str> { + match self { + CoinProtocol::QRC20 { contract_address, .. } | CoinProtocol::ERC20 { contract_address, .. } => { + Some(contract_address) + }, + CoinProtocol::SLPTOKEN { .. } + | CoinProtocol::UTXO + | CoinProtocol::QTUM + | CoinProtocol::ETH + | CoinProtocol::BCH { .. } + | CoinProtocol::TENDERMINT(_) + | CoinProtocol::TENDERMINTTOKEN(_) + | CoinProtocol::ZHTLC(_) + | CoinProtocol::NFT { .. } => None, + #[cfg(not(target_arch = "wasm32"))] + CoinProtocol::LIGHTNING { .. } => None, + #[cfg(feature = "enable-sia")] + CoinProtocol::SIA => None, + } + } + + /// Several checks to be preformed when a custom token is being activated to check uniqueness among other things. + #[allow(clippy::result_large_err)] + pub fn custom_token_validations(&self, ctx: &MmArc) -> MmResult<(), CustomTokenError> { + let CoinProtocol::ERC20 { + platform, + contract_address, + } = self + else { + return Ok(()); + }; + + // Check if there is a token with the same contract address in the config. + // If there is, return an error as the user should use this token instead of activating a custom one. + // This is necessary as we will create an orderbook for this custom token using the contract address, + // if it is duplicated in config, we will have two orderbooks one using the ticker and one using the contract address. + // Todo: We should use the contract address for orderbook topics instead of the ticker once we make custom tokens non-wallet only. + // If a coin is added to the config later, users who added it as a custom token and did not update will not see the orderbook. + if let Some(existing_ticker) = get_erc20_ticker_by_contract_address(ctx, platform, contract_address) { + return Err(MmError::new(CustomTokenError::DuplicateContractInConfig { + ticker_in_config: existing_ticker, + })); + } + + Ok(()) + } +} + /// Common methods to handle the connection events. /// /// Note that the handler methods are sync and shouldn't take long time executing, otherwise it will hurt the performance. @@ -4625,10 +4724,20 @@ pub fn coin_conf(ctx: &MmArc, ticker: &str) -> Json { } } -pub fn is_wallet_only_conf(conf: &Json) -> bool { conf["wallet_only"].as_bool().unwrap_or(false) } +pub fn is_wallet_only_conf(conf: &Json) -> bool { + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if conf.is_null() { + return true; + } + conf["wallet_only"].as_bool().unwrap_or(false) +} pub fn is_wallet_only_ticker(ctx: &MmArc, ticker: &str) -> bool { let coin_conf = coin_conf(ctx, ticker); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } coin_conf["wallet_only"].as_bool().unwrap_or(false) } diff --git a/mm2src/coins/nft.rs b/mm2src/coins/nft.rs index 2dc85e67dc..afc5f260a9 100644 --- a/mm2src/coins/nft.rs +++ b/mm2src/coins/nft.rs @@ -25,6 +25,8 @@ use crate::nft::nft_errors::{ClearNftDbError, MetaFromUrlError, ProtectFromSpamE use crate::nft::nft_structs::{build_nft_with_empty_meta, BuildNftFields, ClearNftDbReq, NftCommon, NftCtx, NftInfo, NftTransferCommon, PhishingDomainReq, PhishingDomainRes, RefreshMetadataReq, SpamContractReq, SpamContractRes, TransferMeta, TransferStatus, UriMeta}; +#[cfg(not(target_arch = "wasm32"))] +use crate::nft::storage::NftMigrationOps; use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; use common::log::error; use common::parse_rfc3339_to_timestamp; @@ -155,6 +157,9 @@ pub async fn get_nft_transfers(ctx: MmArc, req: NftTransfersReq) -> MmResult MmResult<(), UpdateNft let transfer_history_initialized = NftTransferHistoryStorageOps::is_initialized(&storage, chain).await?; let from_block = if transfer_history_initialized { + #[cfg(not(target_arch = "wasm32"))] + NftMigrationOps::migrate_tx_history_if_needed(&storage, chain).await?; let last_transfer_block = NftTransferHistoryStorageOps::get_last_block_number(&storage, chain).await?; last_transfer_block.map(|b| b + 1) } else { diff --git a/mm2src/coins/nft/nft_structs.rs b/mm2src/coins/nft/nft_structs.rs index f772b92f56..e1412933d4 100644 --- a/mm2src/coins/nft/nft_structs.rs +++ b/mm2src/coins/nft/nft_structs.rs @@ -21,6 +21,9 @@ use crate::nft::nft_errors::{LockDBError, ParseChainTypeError, ParseContractType use crate::nft::storage::{NftListStorageOps, NftTransferHistoryStorageOps}; use crate::{TransactionType, TxFeeDetails, WithdrawFee}; +#[cfg(not(target_arch = "wasm32"))] +use crate::nft::storage::NftMigrationOps; + cfg_native! { use db_common::async_sql_conn::AsyncConnection; use futures::lock::Mutex as AsyncMutex; @@ -438,7 +441,8 @@ pub struct WithdrawErc1155 { #[serde(deserialize_with = "deserialize_token_id")] pub(crate) token_id: BigUint, /// Optional amount of the token to withdraw. Defaults to 1 if not specified. - pub(crate) amount: Option, + #[serde(deserialize_with = "deserialize_opt_biguint")] + pub(crate) amount: Option, /// If set to `true`, withdraws the maximum amount available. Overrides the `amount` field. #[serde(default)] pub(crate) max: bool, @@ -489,7 +493,7 @@ pub struct TransactionNftDetails { pub(crate) token_address: String, #[serde(serialize_with = "serialize_token_id")] pub(crate) token_id: BigUint, - pub(crate) amount: BigDecimal, + pub(crate) amount: BigUint, pub(crate) fee_details: Option, /// The coin transaction belongs to pub(crate) coin: String, @@ -730,14 +734,15 @@ impl NftCtx { /// If an `NftCtx` instance doesn't already exist in the MM context, it gets created and cached for subsequent use. #[cfg(not(target_arch = "wasm32"))] pub(crate) fn from_ctx(ctx: &MmArc) -> Result, String> { - Ok(try_s!(from_ctx(&ctx.nft_ctx, move || { + from_ctx(&ctx.nft_ctx, move || { let async_sqlite_connection = ctx .async_sqlite_connection + .get() .ok_or("async_sqlite_connection is not initialized".to_owned())?; Ok(NftCtx { nft_cache_db: async_sqlite_connection.clone(), }) - }))) + }) } #[cfg(target_arch = "wasm32")] @@ -753,7 +758,7 @@ impl NftCtx { #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn lock_db( &self, - ) -> MmResult { + ) -> MmResult { Ok(self.nft_cache_db.lock().await) } @@ -806,6 +811,19 @@ where BigUint::from_str(&s).map_err(serde::de::Error::custom) } +/// Custom deserialization function for optional BigUint. +fn deserialize_opt_biguint<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt: Option = Option::deserialize(deserializer)?; + if let Some(s) = opt { + BigUint::from_str(&s).map(Some).map_err(serde::de::Error::custom) + } else { + Ok(None) + } +} + /// Request parameters for clearing NFT data from the database. #[derive(Debug, Deserialize)] pub struct ClearNftDbReq { diff --git a/mm2src/coins/nft/nft_tests.rs b/mm2src/coins/nft/nft_tests.rs index 05f732a9ee..71001d8f21 100644 --- a/mm2src/coins/nft/nft_tests.rs +++ b/mm2src/coins/nft/nft_tests.rs @@ -463,7 +463,12 @@ cross_test!(test_add_get_transfers, { .clone(); assert_eq!(transfer1.block_number, 28056721); let transfer2 = storage - .get_transfer_by_tx_hash_and_log_index(&chain, TX_HASH.to_string(), LOG_INDEX) + .get_transfer_by_tx_hash_log_index_token_id( + &chain, + TX_HASH.to_string(), + LOG_INDEX, + BigUint::from_str("214300047253").unwrap(), + ) .await .unwrap() .unwrap(); diff --git a/mm2src/coins/nft/storage/db_test_helpers.rs b/mm2src/coins/nft/storage/db_test_helpers.rs index d59b845661..2565be8f2e 100644 --- a/mm2src/coins/nft/storage/db_test_helpers.rs +++ b/mm2src/coins/nft/storage/db_test_helpers.rs @@ -258,7 +258,7 @@ pub(crate) fn nft_transfer_history() -> Vec { transaction_index: Some(198), log_index: 495, value: Default::default(), - transaction_type: Some("Single".to_string()), + transaction_type: Some("Batch".to_string()), token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), @@ -284,15 +284,15 @@ pub(crate) fn nft_transfer_history() -> Vec { confirmations: 0, }; - // Same as transfer1 but with different log_index, meaning that transfer1 and transfer2 are part of one batch/multi token transaction + // Same as transfer1 (identical tx hash and log index) but with different token_id, meaning that transfer1 and transfer2 are part of one batch/multi token transaction let transfer2 = NftTransferHistory { common: NftTransferCommon { block_hash: Some("0x3d68b78391fb3cf8570df27036214f7e9a5a6a45d309197936f51d826041bfe7".to_string()), transaction_hash: "0x1e9f04e9b571b283bde02c98c2a97da39b2bb665b57c1f2b0b733f9b681debbe".to_string(), transaction_index: Some(198), - log_index: 496, + log_index: 495, value: Default::default(), - transaction_type: Some("Single".to_string()), + transaction_type: Some("Batch".to_string()), token_address: Address::from_str("0xfd913a305d70a60aac4faac70c739563738e1f81").unwrap(), from_address: Address::from_str("0x6fad0ec6bb76914b2a2a800686acc22970645820").unwrap(), to_address: Address::from_str("0xf622a6c52c94b500542e2ae6bcad24c53bc5b6a2").unwrap(), diff --git a/mm2src/coins/nft/storage/mod.rs b/mm2src/coins/nft/storage/mod.rs index ad255100c3..3eed941b5d 100644 --- a/mm2src/coins/nft/storage/mod.rs +++ b/mm2src/coins/nft/storage/mod.rs @@ -156,11 +156,12 @@ pub trait NftTransferHistoryStorageOps { token_id: BigUint, ) -> MmResult, Self::Error>; - async fn get_transfer_by_tx_hash_and_log_index( + async fn get_transfer_by_tx_hash_log_index_token_id( &self, chain: &Chain, transaction_hash: String, log_index: u32, + token_id: BigUint, ) -> MmResult, Self::Error>; /// Updates the metadata for NFT transfers identified by their token address and ID. @@ -243,3 +244,10 @@ pub(crate) struct TransferDetailsJson { pub(crate) to_address: Address, pub(crate) fee_details: Option, } + +#[async_trait] +pub trait NftMigrationOps { + type Error: NftStorageError; + + async fn migrate_tx_history_if_needed(&self, chain: &Chain) -> MmResult<(), Self::Error>; +} diff --git a/mm2src/coins/nft/storage/sql_storage.rs b/mm2src/coins/nft/storage/sql_storage.rs index 6844b261d9..bffdaef27b 100644 --- a/mm2src/coins/nft/storage/sql_storage.rs +++ b/mm2src/coins/nft/storage/sql_storage.rs @@ -2,10 +2,10 @@ use crate::nft::eth_addr_to_hex; use crate::nft::nft_structs::{Chain, ContractType, ConvertChain, Nft, NftCommon, NftList, NftListFilters, NftTokenAddrId, NftTransferCommon, NftTransferHistory, NftTransferHistoryFilters, NftsTransferHistoryList, TransferMeta, UriMeta}; -use crate::nft::storage::{get_offset_limit, NftDetailsJson, NftListStorageOps, NftStorageError, +use crate::nft::storage::{get_offset_limit, NftDetailsJson, NftListStorageOps, NftMigrationOps, NftStorageError, NftTransferHistoryStorageOps, RemoveNftResult, TransferDetailsJson}; use async_trait::async_trait; -use db_common::async_sql_conn::{AsyncConnError, AsyncConnection}; +use db_common::async_sql_conn::{AsyncConnError, AsyncConnection, InternalError}; use db_common::sql_build::{SqlCondition, SqlQuery}; use db_common::sqlite::rusqlite::types::{FromSqlError, Type}; use db_common::sqlite::rusqlite::{Connection, Error as SqlError, Result as SqlResult, Row, Statement}; @@ -22,6 +22,8 @@ use std::convert::TryInto; use std::num::NonZeroUsize; use std::str::FromStr; +const CURRENT_SCHEMA_VERSION_TX_HISTORY: i32 = 2; + impl Chain { fn nft_list_table_name(&self) -> SqlResult { let name = self.to_ticker().to_owned() + "_nft_list"; @@ -42,6 +44,12 @@ fn scanned_nft_blocks_table_name() -> SqlResult { Ok(safe_name) } +fn schema_versions_table_name() -> SqlResult { + let name = "schema_versions".to_string(); + let safe_name = SafeTableName::new(&name)?; + Ok(safe_name) +} + fn create_nft_list_table_sql(chain: &Chain) -> MmResult { let safe_table_name = chain.nft_list_table_name()?; let sql = format!( @@ -82,6 +90,11 @@ fn create_nft_list_table_sql(chain: &Chain) -> MmResult { fn create_transfer_history_table_sql(chain: &Chain) -> Result { let safe_table_name = chain.transfer_history_table_name()?; + create_transfer_history_table_sql_custom_name(&safe_table_name) +} + +/// Supports [CURRENT_SCHEMA_VERSION_TX_HISTORY] +fn create_transfer_history_table_sql_custom_name(safe_table_name: &SafeTableName) -> Result { let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( transaction_hash VARCHAR(256) NOT NULL, @@ -103,7 +116,7 @@ fn create_transfer_history_table_sql(chain: &Chain) -> Result image_domain TEXT, token_name TEXT, details_json TEXT, - PRIMARY KEY (transaction_hash, log_index) + PRIMARY KEY (transaction_hash, log_index, token_id) );", safe_table_name.inner() ); @@ -122,6 +135,18 @@ fn create_scanned_nft_blocks_sql() -> Result { Ok(sql) } +fn create_schema_versions_sql() -> Result { + let safe_table_name = schema_versions_table_name()?; + let sql = format!( + "CREATE TABLE IF NOT EXISTS {} ( + table_name TEXT PRIMARY KEY, + version INTEGER NOT NULL + );", + safe_table_name.inner() + ); + Ok(sql) +} + impl NftStorageError for AsyncConnError {} fn get_nft_list_builder_preimage(chains: Vec, filters: Option) -> Result { @@ -432,6 +457,15 @@ fn upsert_last_scanned_block_sql() -> Result { Ok(sql) } +fn insert_schema_version_sql() -> Result { + let schema_table = schema_versions_table_name()?; + let sql = format!( + "INSERT INTO {} (table_name, version) VALUES (?1, ?2) ON CONFLICT(table_name) DO NOTHING;", + schema_table.inner() + ); + Ok(sql) +} + fn refresh_nft_metadata_sql(chain: &Chain) -> Result { let safe_table_name = chain.nft_list_table_name()?; let sql = format!( @@ -462,12 +496,25 @@ fn update_transfer_spam_by_token_addr_id(chain: &Chain) -> Result Result { - let sql = format!( +/// Generates the SQL command to insert or update the schema version in the `schema_versions` table. +/// +/// This function creates an SQL command that attempts to insert a new row with the specified +/// `table_name` and `version`. If a row with the same `table_name` already exists, the `version` +/// field is updated to the new value provided. +fn update_schema_version_sql(schema_versions: &SafeTableName) -> String { + format!( + "INSERT INTO {} (table_name, version) + VALUES (?1, ?2) + ON CONFLICT(table_name) DO UPDATE SET version = excluded.version;", + schema_versions.inner() + ) +} + +fn select_last_block_number_sql(safe_table_name: SafeTableName) -> String { + format!( "SELECT block_number FROM {} ORDER BY block_number DESC LIMIT 1", safe_table_name.inner() - ); - Ok(sql) + ) } fn select_last_scanned_block_sql() -> MmResult { @@ -540,6 +587,13 @@ fn get_transfers_with_empty_meta_builder<'a>(conn: &'a Connection, chain: &'a Ch Ok(sql_builder) } +fn get_schema_version_stmt(conn: &Connection) -> Result { + let table_name = schema_versions_table_name()?; + let sql = format!("SELECT version FROM {} WHERE table_name = ?1;", table_name.inner()); + let stmt = conn.prepare(&sql)?; + Ok(stmt) +} + fn is_table_empty(conn: &Connection, safe_table_name: SafeTableName) -> Result { let query = format!("SELECT COUNT(*) FROM {}", safe_table_name.inner()); conn.query_row(&query, [], |row| row.get::<_, i64>(0)) @@ -777,7 +831,7 @@ impl NftListStorageOps for AsyncMutexGuard<'_, AsyncConnection> { async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { let table_name = chain.nft_list_table_name()?; - let sql = select_last_block_number_sql(table_name)?; + let sql = select_last_block_number_sql(table_name); self.call(move |conn| { let block_number = query_single_row(conn, &sql, [], block_number_from_row)?; Ok(block_number) @@ -969,8 +1023,15 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { async fn init(&self, chain: &Chain) -> MmResult<(), Self::Error> { let sql_transfer_history = create_transfer_history_table_sql(chain)?; + let table_name = chain.transfer_history_table_name()?; self.call(move |conn| { conn.execute(&sql_transfer_history, []).map(|_| ())?; + conn.execute(&create_schema_versions_sql()?, []).map(|_| ())?; + conn.execute(&insert_schema_version_sql()?, [ + table_name.inner(), + &CURRENT_SCHEMA_VERSION_TX_HISTORY.to_string(), + ]) + .map(|_| ())?; Ok(()) }) .await @@ -978,11 +1039,10 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { } async fn is_initialized(&self, chain: &Chain) -> MmResult { - let table_name = chain.transfer_history_table_name()?; + let table = chain.transfer_history_table_name()?; self.call(move |conn| { - let nft_list_initialized = - query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table_name.inner()], string_from_row)?; - Ok(nft_list_initialized.is_some()) + let table_exists = query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [table.inner()], string_from_row)?; + Ok(table_exists.is_some()) }) .await .map_to_mm(AsyncConnError::from) @@ -1077,7 +1137,7 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { async fn get_last_block_number(&self, chain: &Chain) -> MmResult, Self::Error> { let table_name = chain.transfer_history_table_name()?; - let sql = select_last_block_number_sql(table_name)?; + let sql = select_last_block_number_sql(table_name); self.call(move |conn| { let block_number = query_single_row(conn, &sql, [], block_number_from_row)?; Ok(block_number) @@ -1121,22 +1181,23 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { .map_to_mm(AsyncConnError::from) } - async fn get_transfer_by_tx_hash_and_log_index( + async fn get_transfer_by_tx_hash_log_index_token_id( &self, chain: &Chain, transaction_hash: String, log_index: u32, + token_id: BigUint, ) -> MmResult, Self::Error> { let table_name = chain.transfer_history_table_name()?; let sql = format!( - "SELECT * FROM {} WHERE transaction_hash=?1 AND log_index = ?2", + "SELECT * FROM {} WHERE transaction_hash=?1 AND log_index = ?2 AND token_id = ?3", table_name.inner() ); self.call(move |conn| { let transfer = query_single_row( conn, &sql, - [transaction_hash, log_index.to_string()], + [transaction_hash, log_index.to_string(), token_id.to_string()], transfer_history_from_row, )?; Ok(transfer) @@ -1285,11 +1346,18 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { } async fn clear_history_data(&self, chain: &Chain) -> MmResult<(), Self::Error> { - let table_name = chain.transfer_history_table_name()?; + let history_table_name = chain.transfer_history_table_name()?; + let schema_table_name = schema_versions_table_name()?; + let dlt_schema_sql = format!("DELETE from {} where table_name=?1", schema_table_name.inner()); self.call(move |conn| { let sql_transaction = conn.transaction()?; - sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_name.inner()), [])?; + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", history_table_name.inner()), [])?; + sql_transaction.execute(&dlt_schema_sql, [history_table_name.inner()])?; sql_transaction.commit()?; + if is_table_empty(conn, schema_table_name.clone())? { + conn.execute(&format!("DROP TABLE IF EXISTS {};", schema_table_name.inner()), []) + .map(|_| ())?; + } Ok(()) }) .await @@ -1297,12 +1365,14 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { } async fn clear_all_history_data(&self) -> MmResult<(), Self::Error> { + let schema_table = schema_versions_table_name()?; self.call(move |conn| { let sql_transaction = conn.transaction()?; for chain in Chain::variant_list().into_iter() { let table_name = chain.transfer_history_table_name()?; sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", table_name.inner()), [])?; } + sql_transaction.execute(&format!("DROP TABLE IF EXISTS {};", schema_table.inner()), [])?; sql_transaction.commit()?; Ok(()) }) @@ -1310,3 +1380,112 @@ impl NftTransferHistoryStorageOps for AsyncMutexGuard<'_, AsyncConnection> { .map_to_mm(AsyncConnError::from) } } + +fn migrate_tx_history_table_from_schema_0_to_2( + conn: &mut Connection, + history_table: &SafeTableName, + schema_table: &SafeTableName, +) -> Result<(), AsyncConnError> { + if has_primary_key_duplication(conn, history_table)? { + return Err(AsyncConnError::Internal(InternalError( + "Primary key duplication occurred in old nft tx history table".to_string(), + ))); + } + + // Start a transaction to ensure all operations are atomic + let sql_tx = conn.transaction()?; + + // Create the temporary table with the new schema + let temp_table_name = SafeTableName::new(format!("{}_temp", history_table.inner()).as_str())?; + sql_tx.execute(&create_transfer_history_table_sql_custom_name(&temp_table_name)?, [])?; + + // I don't think we need to batch the data copy process here. + // It's unlikely that the table will grow to 1 million+ rows (as an example). + let copy_data_sql = format!( + "INSERT INTO {} SELECT * FROM {};", + temp_table_name.inner(), + history_table.inner() + ); + sql_tx.execute(©_data_sql, [])?; + + let drop_old_table_sql = format!("DROP TABLE IF EXISTS {};", history_table.inner()); + sql_tx.execute(&drop_old_table_sql, [])?; + + let rename_table_sql = format!( + "ALTER TABLE {} RENAME TO {};", + temp_table_name.inner(), + history_table.inner() + ); + sql_tx.execute(&rename_table_sql, [])?; + + sql_tx.execute(&update_schema_version_sql(schema_table), [ + history_table.inner().to_string(), + CURRENT_SCHEMA_VERSION_TX_HISTORY.to_string(), + ])?; + + sql_tx.commit()?; + + Ok(()) +} + +/// Query to check for duplicates based on the primary key columns from tx history table version 2 +fn has_primary_key_duplication(conn: &Connection, safe_table_name: &SafeTableName) -> Result { + let query = format!( + "SELECT EXISTS ( + SELECT 1 + FROM {} + GROUP BY transaction_hash, log_index, token_id + HAVING COUNT(*) > 1 + );", + safe_table_name.inner() + ); + // return true if duplicates exist, false otherwise + conn.query_row(&query, [], |row| row.get::<_, i32>(0)) + .map(|exists| exists == 1) +} + +#[async_trait] +impl NftMigrationOps for AsyncMutexGuard<'_, AsyncConnection> { + type Error = AsyncConnError; + + async fn migrate_tx_history_if_needed(&self, chain: &Chain) -> MmResult<(), Self::Error> { + let history_table = chain.transfer_history_table_name()?; + let schema_table = schema_versions_table_name()?; + self.call(move |conn| { + let schema_table_exists = + query_single_row(conn, CHECK_TABLE_EXISTS_SQL, [schema_table.inner()], string_from_row)?; + + let mut version = if schema_table_exists.is_some() { + get_schema_version_stmt(conn)? + .query_row([history_table.inner()], |row| row.get(0)) + .unwrap_or(0) + } else { + conn.execute(&create_schema_versions_sql()?, []).map(|_| ())?; + 0 + }; + + while version < CURRENT_SCHEMA_VERSION_TX_HISTORY { + match version { + 0 => { + migrate_tx_history_table_from_schema_0_to_2(conn, &history_table, &schema_table)?; + }, + 1 => { + // The Tx History SQL schema didn't have version 1, but let's handle this case + // for consistency with IndexedDB versioning, where the current Tx History schema is at version 2. + }, + unsupported_version => { + return Err(AsyncConnError::Internal(InternalError(format!( + "Unsupported schema version {}", + unsupported_version + )))); + }, + } + version += 1; + } + + Ok(()) + }) + .await + .map_to_mm(AsyncConnError::from) + } +} diff --git a/mm2src/coins/nft/storage/wasm/nft_idb.rs b/mm2src/coins/nft/storage/wasm/nft_idb.rs index 054f1c058e..775a871589 100644 --- a/mm2src/coins/nft/storage/wasm/nft_idb.rs +++ b/mm2src/coins/nft/storage/wasm/nft_idb.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use mm2_db::indexed_db::InitDbResult; use mm2_db::indexed_db::{DbIdentifier, DbInstance, DbLocked, IndexedDb, IndexedDbBuilder}; -const DB_VERSION: u32 = 1; +/// prim key was changed in NftTransferHistoryTable, schemas of the other tables remain the same. +const DB_VERSION: u32 = 2; /// Represents a locked instance of the `NftCacheIDB` database. /// diff --git a/mm2src/coins/nft/storage/wasm/wasm_storage.rs b/mm2src/coins/nft/storage/wasm/wasm_storage.rs index e5ea955918..99e76ba04f 100644 --- a/mm2src/coins/nft/storage/wasm/wasm_storage.rs +++ b/mm2src/coins/nft/storage/wasm/wasm_storage.rs @@ -6,11 +6,10 @@ use crate::nft::storage::wasm::{WasmNftCacheError, WasmNftCacheResult}; use crate::nft::storage::{get_offset_limit, NftListStorageOps, NftTokenAddrId, NftTransferHistoryFilters, NftTransferHistoryStorageOps, RemoveNftResult}; use async_trait::async_trait; -use common::is_initial_upgrade; use ethereum_types::Address; -use mm2_db::indexed_db::{BeBigUint, DbTable, DbUpgrader, MultiIndex, OnUpgradeResult, TableSignature}; +use mm2_db::indexed_db::{BeBigUint, DbTable, DbUpgrader, MultiIndex, OnUpgradeError, OnUpgradeResult, TableSignature}; use mm2_err_handle::map_to_mm::MapToMmResult; -use mm2_err_handle::prelude::MmResult; +use mm2_err_handle::prelude::{MmError, MmResult}; use mm2_number::BigUint; use num_traits::ToPrimitive; use serde_json::{self as json, Value as Json}; @@ -547,18 +546,20 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { .collect() } - async fn get_transfer_by_tx_hash_and_log_index( + async fn get_transfer_by_tx_hash_log_index_token_id( &self, chain: &Chain, transaction_hash: String, log_index: u32, + token_id: BigUint, ) -> MmResult, Self::Error> { let db_transaction = self.get_inner().transaction().await?; let table = db_transaction.table::().await?; - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX) .with_value(chain.to_string())? .with_value(&transaction_hash)? - .with_value(log_index)?; + .with_value(log_index)? + .with_value(BeBigUint::from(token_id))?; if let Some((_item_id, item)) = table.get_item_by_unique_multi_index(index_keys).await? { Ok(Some(transfer_details_from_item(item)?)) @@ -602,10 +603,11 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { } drop_mutability!(transfer); - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX) .with_value(&chain_str)? .with_value(&transfer.common.transaction_hash)? - .with_value(transfer.common.log_index)?; + .with_value(transfer.common.log_index)? + .with_value(BeBigUint::from(transfer.token_id.clone()))?; let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; table.replace_item_by_unique_multi_index(index_keys, &item).await?; @@ -691,10 +693,11 @@ impl NftTransferHistoryStorageOps for NftCacheIDBLocked<'_> { transfer.common.possible_spam = possible_spam; drop_mutability!(transfer); - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX) .with_value(&chain_str)? .with_value(&transfer.common.transaction_hash)? - .with_value(transfer.common.log_index)?; + .with_value(transfer.common.log_index)? + .with_value(BeBigUint::from(transfer.token_id.clone()))?; let item = NftTransferHistoryTable::from_transfer_history(&transfer)?; table.replace_item_by_unique_multi_index(index_keys, &item).await?; @@ -777,10 +780,11 @@ async fn update_transfer_phishing_for_index( transfer.possible_phishing = possible_phishing; drop_mutability!(transfer); let transfer_item = NftTransferHistoryTable::from_transfer_history(&transfer)?; - let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_INDEX) + let index_keys = MultiIndex::new(NftTransferHistoryTable::CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX) .with_value(chain)? .with_value(&transfer.common.transaction_hash)? - .with_value(transfer.common.log_index)?; + .with_value(transfer.common.log_index)? + .with_value(BeBigUint::from(transfer.token_id))?; table .replace_item_by_unique_multi_index(index_keys, &transfer_item) .await?; @@ -876,8 +880,8 @@ pub(crate) struct NftListTable { } impl NftListTable { - const CHAIN_ANIMATION_DOMAIN_INDEX: &str = "chain_animation_domain_index"; - const CHAIN_EXTERNAL_DOMAIN_INDEX: &str = "chain_external_domain_index"; + const CHAIN_ANIMATION_DOMAIN_INDEX: &'static str = "chain_animation_domain_index"; + const CHAIN_EXTERNAL_DOMAIN_INDEX: &'static str = "chain_external_domain_index"; fn from_nft(nft: &Nft) -> WasmNftCacheResult { let details_json = json::to_value(nft).map_to_mm(|e| WasmNftCacheError::ErrorSerializing(e.to_string()))?; @@ -902,26 +906,45 @@ impl NftListTable { impl TableSignature for NftListTable { const TABLE_NAME: &'static str = "nft_list_cache_table"; - fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::TABLE_NAME)?; - table.create_multi_index( - CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, - &["chain", "token_address", "token_id"], - true, - )?; - table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; - table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; - table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; - table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; - table.create_multi_index( - Self::CHAIN_ANIMATION_DOMAIN_INDEX, - &["chain", "animation_domain"], - false, - )?; - table.create_multi_index(Self::CHAIN_EXTERNAL_DOMAIN_INDEX, &["chain", "external_domain"], false)?; - table.create_index("chain", false)?; - table.create_index("block_number", false)?; + fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + while old_version < new_version { + match old_version { + 0 => { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index( + CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + &["chain", "token_address", "token_id"], + true, + )?; + table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; + table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; + table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; + table.create_multi_index( + Self::CHAIN_ANIMATION_DOMAIN_INDEX, + &["chain", "animation_domain"], + false, + )?; + table.create_multi_index( + Self::CHAIN_EXTERNAL_DOMAIN_INDEX, + &["chain", "external_domain"], + false, + )?; + table.create_index("chain", false)?; + table.create_index("block_number", false)?; + }, + 1 => { + // nothing to change + }, + unsupported_version => { + return MmError::err(OnUpgradeError::UnsupportedVersion { + unsupported_version, + old_version, + new_version, + }) + }, + } + old_version += 1; } Ok(()) } @@ -951,7 +974,10 @@ pub(crate) struct NftTransferHistoryTable { } impl NftTransferHistoryTable { - const CHAIN_TX_HASH_LOG_INDEX_INDEX: &str = "chain_tx_hash_log_index_index"; + // old prim key index for DB_VERSION = 1 + const CHAIN_TX_HASH_LOG_INDEX_INDEX: &'static str = "chain_tx_hash_log_index_index"; + // prim key multi index for DB_VERSION = 2 + const CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX: &'static str = "chain_tx_hash_log_index_token_id_index"; fn from_transfer_history(transfer: &NftTransferHistory) -> WasmNftCacheResult { let details_json = @@ -983,25 +1009,47 @@ impl NftTransferHistoryTable { impl TableSignature for NftTransferHistoryTable { const TABLE_NAME: &'static str = "nft_transfer_history_cache_table"; - fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::TABLE_NAME)?; - table.create_multi_index( - CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, - &["chain", "token_address", "token_id"], - false, - )?; - table.create_multi_index( - Self::CHAIN_TX_HASH_LOG_INDEX_INDEX, - &["chain", "transaction_hash", "log_index"], - true, - )?; - table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; - table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; - table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; - table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; - table.create_index("block_number", false)?; - table.create_index("chain", false)?; + fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + while old_version < new_version { + match old_version { + 0 => { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_multi_index( + Self::CHAIN_TX_HASH_LOG_INDEX_INDEX, + &["chain", "transaction_hash", "log_index"], + true, + )?; + table.create_multi_index( + CHAIN_TOKEN_ADD_TOKEN_ID_INDEX, + &["chain", "token_address", "token_id"], + false, + )?; + table.create_multi_index(CHAIN_BLOCK_NUMBER_INDEX, &["chain", "block_number"], false)?; + table.create_multi_index(CHAIN_TOKEN_ADD_INDEX, &["chain", "token_address"], false)?; + table.create_multi_index(CHAIN_TOKEN_DOMAIN_INDEX, &["chain", "token_domain"], false)?; + table.create_multi_index(CHAIN_IMAGE_DOMAIN_INDEX, &["chain", "image_domain"], false)?; + table.create_index("block_number", false)?; + table.create_index("chain", false)?; + }, + 1 => { + let table = upgrader.open_table(Self::TABLE_NAME)?; + // When we change indexes during `onupgradeneeded`, IndexedDB automatically updates it with the existing records + table.create_multi_index( + Self::CHAIN_TX_HASH_LOG_INDEX_TOKEN_ID_INDEX, + &["chain", "transaction_hash", "log_index", "token_id"], + true, + )?; + table.delete_index(Self::CHAIN_TX_HASH_LOG_INDEX_INDEX)?; + }, + unsupported_version => { + return MmError::err(OnUpgradeError::UnsupportedVersion { + unsupported_version, + old_version, + new_version, + }) + }, + } + old_version += 1; } Ok(()) } @@ -1016,10 +1064,25 @@ pub(crate) struct LastScannedBlockTable { impl TableSignature for LastScannedBlockTable { const TABLE_NAME: &'static str = "last_scanned_block_table"; - fn on_upgrade_needed(upgrader: &DbUpgrader, old_version: u32, new_version: u32) -> OnUpgradeResult<()> { - if is_initial_upgrade(old_version, new_version) { - let table = upgrader.create_table(Self::TABLE_NAME)?; - table.create_index("chain", true)?; + fn on_upgrade_needed(upgrader: &DbUpgrader, mut old_version: u32, new_version: u32) -> OnUpgradeResult<()> { + while old_version < new_version { + match old_version { + 0 => { + let table = upgrader.create_table(Self::TABLE_NAME)?; + table.create_index("chain", true)?; + }, + 1 => { + // nothing to change + }, + unsupported_version => { + return MmError::err(OnUpgradeError::UnsupportedVersion { + unsupported_version, + old_version, + new_version, + }) + }, + } + old_version += 1; } Ok(()) } diff --git a/mm2src/coins/qrc20.rs b/mm2src/coins/qrc20.rs index df1740736e..08370c2b90 100644 --- a/mm2src/coins/qrc20.rs +++ b/mm2src/coins/qrc20.rs @@ -1248,23 +1248,11 @@ impl MarketCoinOps for Qrc20Coin { Box::new(fut.boxed().compat()) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let tx: UtxoTx = try_tx_fus!(deserialize(args.tx_bytes).map_err(|e| ERRL!("{:?}", e))); - - let selfi = self.clone(); - let WaitForHTLCTxSpendArgs { - check_every, - from_block, - wait_until, - .. - } = args; - let fut = async move { - selfi - .wait_for_tx_spend_impl(tx, wait_until, from_block, check_every) - .map_err(TransactionErr::Plain) - .await - }; - Box::new(fut.boxed().compat()) + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + let tx: UtxoTx = try_tx_s!(deserialize(args.tx_bytes).map_err(|e| ERRL!("{:?}", e))); + self.wait_for_tx_spend_impl(tx, args.wait_until, args.from_block, args.check_every) + .map_err(TransactionErr::Plain) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/qrc20/qrc20_tests.rs b/mm2src/coins/qrc20/qrc20_tests.rs index 976253cf32..c26f5010e0 100644 --- a/mm2src/coins/qrc20/qrc20_tests.rs +++ b/mm2src/coins/qrc20/qrc20_tests.rs @@ -453,7 +453,7 @@ fn test_wait_for_tx_spend_malicious() { let payment_tx = hex::decode("01000000016601daa208531d20532c460d0c86b74a275f4a126bbffcf4eafdf33835af2859010000006a47304402205825657548bc1b5acf3f4bb2f89635a02b04f3228cd08126e63c5834888e7ac402207ca05fa0a629a31908a97a508e15076e925f8e621b155312b7526a6666b06a76012103693bff1b39e8b5a306810023c29b95397eb395530b106b1820ea235fd81d9ce9ffffffff020000000000000000e35403a0860101284cc49b415b2a8620ad3b72361a5aeba5dffd333fb64750089d935a1ec974d6a91ef4f24ff6ba0000000000000000000000000000000000000000000000000000000001312d00000000000000000000000000d362e096e873eb7907e205fadc6175c6fec7bc44000000000000000000000000783cf0be521101942da509846ea476e683aad8324b6b2e5444c2639cc0fb7bcea5afba3f3cdce239000000000000000000000000000000000000000000000000000000000000000000000000000000005f855c7614ba8b71f3544b93e2f681f996da519a98ace0107ac2203de400000000001976a9149e032d4b0090a11dc40fe6c47601499a35d55fbb88ac415d855f").unwrap(); let wait_until = now_sec() + 1; let from_block = 696245; - let found = block_on_f01(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + let found = block_on(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &payment_tx, secret_hash: &[], wait_until, diff --git a/mm2src/coins/rpc_command/get_estimated_fees.rs b/mm2src/coins/rpc_command/get_estimated_fees.rs index b62e572756..811ecb448e 100644 --- a/mm2src/coins/rpc_command/get_estimated_fees.rs +++ b/mm2src/coins/rpc_command/get_estimated_fees.rs @@ -1,7 +1,7 @@ //! RPCs to start/stop gas fee estimator and get estimated base and priority fee per gas -use crate::eth::{EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; -use crate::{lp_coinfind_or_err, wei_to_gwei_decimal, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; +use crate::eth::{wei_to_gwei_decimal, EthCoin, EthCoinType, FeeEstimatorContext, FeeEstimatorState, FeePerGasEstimated}; +use crate::{lp_coinfind_or_err, AsyncMutex, CoinFindError, MmCoinEnum, NumConversError}; use common::executor::{spawn_abortable, Timer}; use common::log::debug; use common::{HttpStatusCode, StatusCode}; @@ -66,22 +66,22 @@ impl TryFrom for FeePerGasEstimatedExt { fn try_from(fees: FeePerGasEstimated) -> Result { Ok(Self { - base_fee: wei_to_gwei_decimal!(fees.base_fee)?, + base_fee: wei_to_gwei_decimal(fees.base_fee)?, low: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.low.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.low.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.low.max_priority_fee_per_gas)?, min_wait_time: fees.low.min_wait_time, max_wait_time: fees.low.max_wait_time, }, medium: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.medium.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.medium.max_priority_fee_per_gas)?, min_wait_time: fees.medium.min_wait_time, max_wait_time: fees.medium.max_wait_time, }, high: FeePerGasLevel { - max_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_fee_per_gas)?, - max_priority_fee_per_gas: wei_to_gwei_decimal!(fees.high.max_priority_fee_per_gas)?, + max_fee_per_gas: wei_to_gwei_decimal(fees.high.max_fee_per_gas)?, + max_priority_fee_per_gas: wei_to_gwei_decimal(fees.high.max_priority_fee_per_gas)?, min_wait_time: fees.high.min_wait_time, max_wait_time: fees.high.max_wait_time, }, diff --git a/mm2src/coins/rpc_command/get_new_address.rs b/mm2src/coins/rpc_command/get_new_address.rs index e37efb730f..35796de9c2 100644 --- a/mm2src/coins/rpc_command/get_new_address.rs +++ b/mm2src/coins/rpc_command/get_new_address.rs @@ -472,7 +472,7 @@ pub(crate) mod common_impl { Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: address.to_string(), + address: coin.address_formatter()(&address), derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, @@ -510,13 +510,14 @@ pub(crate) mod common_impl { let address = hd_address.address(); let balance = coin.known_address_balance(&address).await?; - coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([address.to_string()])) + let formatted_address = coin.address_formatter()(&address); + coin.prepare_addresses_for_balance_stream_if_enabled(HashSet::from([formatted_address.clone()])) .await .map_err(|e| GetNewAddressRpcError::FailedScripthashSubscription(e.to_string()))?; Ok(GetNewAddressResponse { new_address: HDAddressBalance { - address: address.to_string(), + address: formatted_address, derivation_path: RpcDerivationPath(hd_address.derivation_path().clone()), chain, balance, diff --git a/mm2src/coins/rpc_command/tendermint/mod.rs b/mm2src/coins/rpc_command/tendermint/mod.rs index 3e2b664aec..9a3d714bd3 100644 --- a/mm2src/coins/rpc_command/tendermint/mod.rs +++ b/mm2src/coins/rpc_command/tendermint/mod.rs @@ -1,5 +1,6 @@ mod ibc_chains; mod ibc_transfer_channels; +pub mod staking; pub use ibc_chains::*; pub use ibc_transfer_channels::*; diff --git a/mm2src/coins/rpc_command/tendermint/staking.rs b/mm2src/coins/rpc_command/tendermint/staking.rs new file mode 100644 index 0000000000..c6ac3dca4e --- /dev/null +++ b/mm2src/coins/rpc_command/tendermint/staking.rs @@ -0,0 +1,150 @@ +use common::{HttpStatusCode, PagingOptions, StatusCode}; +use cosmrs::staking::{Commission, Description, Validator}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::MmError; + +use crate::{lp_coinfind_or_err, tendermint::TendermintCoinRpcError, MmCoinEnum}; + +/// Represents current status of the validator. +#[derive(Default, Deserialize)] +pub(crate) enum ValidatorStatus { + All, + /// Validator is in the active set and participates in consensus. + #[default] + Bonded, + /// Validator is not in the active set and does not participate in consensus. + /// Accordingly, they do not receive rewards and cannot be slashed. + /// It is still possible to delegate tokens to a validator in this state. + Unbonded, +} + +impl ToString for ValidatorStatus { + fn to_string(&self) -> String { + match self { + // An empty string doesn't filter any validators and we get an unfiltered result. + ValidatorStatus::All => String::default(), + ValidatorStatus::Bonded => "BOND_STATUS_BONDED".into(), + ValidatorStatus::Unbonded => "BOND_STATUS_UNBONDED".into(), + } + } +} + +#[derive(Deserialize)] +pub struct ValidatorsRPC { + #[serde(rename = "ticker")] + coin: String, + #[serde(flatten)] + paging: PagingOptions, + #[serde(default)] + filter_by_status: ValidatorStatus, +} + +#[derive(Clone, Serialize)] +pub struct ValidatorsRPCResponse { + validators: Vec, +} + +#[derive(Clone, Debug, Display, Serialize, SerializeErrorType, PartialEq)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ValidatorsRPCError { + #[display(fmt = "Coin '{ticker}' could not be found in coins configuration.")] + CoinNotFound { ticker: String }, + #[display(fmt = "'{ticker}' is not a Cosmos coin.")] + UnexpectedCoinType { ticker: String }, + #[display(fmt = "Transport error: {}", _0)] + Transport(String), + #[display(fmt = "Internal error: {}", _0)] + InternalError(String), +} + +impl HttpStatusCode for ValidatorsRPCError { + fn status_code(&self) -> common::StatusCode { + match self { + ValidatorsRPCError::Transport(_) => StatusCode::SERVICE_UNAVAILABLE, + ValidatorsRPCError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR, + ValidatorsRPCError::CoinNotFound { .. } => StatusCode::NOT_FOUND, + ValidatorsRPCError::UnexpectedCoinType { .. } => StatusCode::BAD_REQUEST, + } + } +} + +impl From for ValidatorsRPCError { + fn from(e: TendermintCoinRpcError) -> Self { + match e { + TendermintCoinRpcError::InvalidResponse(e) + | TendermintCoinRpcError::PerformError(e) + | TendermintCoinRpcError::RpcClientError(e) => ValidatorsRPCError::Transport(e), + TendermintCoinRpcError::Prost(e) | TendermintCoinRpcError::InternalError(e) => ValidatorsRPCError::InternalError(e), + TendermintCoinRpcError::UnexpectedAccountType { .. } => ValidatorsRPCError::InternalError( + "RPC client got an unexpected error 'TendermintCoinRpcError::UnexpectedAccountType', this isn't normal." + .into(), + ), + } + } +} + +pub async fn validators_rpc( + ctx: MmArc, + req: ValidatorsRPC, +) -> Result> { + fn maybe_jsonize_description(description: Option) -> Option { + description.map(|d| { + json!({ + "moniker": d.moniker, + "identity": d.identity, + "website": d.website, + "security_contact": d.security_contact, + "details": d.details, + }) + }) + } + + fn maybe_jsonize_commission(commission: Option) -> Option { + commission.map(|c| { + let rates = c.commission_rates.map(|cr| { + json!({ + "rate": cr.rate, + "max_rate": cr.max_rate, + "max_change_rate": cr.max_change_rate + }) + }); + + json!({ + "commission_rates": rates, + "update_time": c.update_time + }) + }) + } + + fn jsonize_validator(v: Validator) -> serde_json::Value { + json!({ + "operator_address": v.operator_address, + "consensus_pubkey": v.consensus_pubkey, + "jailed": v.jailed, + "status": v.status, + "tokens": v.tokens, + "delegator_shares": v.delegator_shares, + "description": maybe_jsonize_description(v.description), + "unbonding_height": v.unbonding_height, + "unbonding_time": v.unbonding_time, + "commission": maybe_jsonize_commission(v.commission), + "min_self_delegation": v.min_self_delegation, + }) + } + + let validators = match lp_coinfind_or_err(&ctx, &req.coin).await { + Ok(MmCoinEnum::Tendermint(coin)) => coin.validators_list(req.filter_by_status, req.paging).await?, + Ok(MmCoinEnum::TendermintToken(token)) => { + token + .platform_coin + .validators_list(req.filter_by_status, req.paging) + .await? + }, + Ok(_) => return MmError::err(ValidatorsRPCError::UnexpectedCoinType { ticker: req.coin }), + Err(_) => return MmError::err(ValidatorsRPCError::CoinNotFound { ticker: req.coin }), + }; + + Ok(ValidatorsRPCResponse { + validators: validators.into_iter().map(jsonize_validator).collect(), + }) +} diff --git a/mm2src/coins/siacoin.rs b/mm2src/coins/siacoin.rs index 2253d6138c..6673cba8c5 100644 --- a/mm2src/coins/siacoin.rs +++ b/mm2src/coins/siacoin.rs @@ -370,7 +370,7 @@ impl MarketCoinOps for SiaCoin { unimplemented!() } - fn wait_for_htlc_tx_spend(&self, _args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } + async fn wait_for_htlc_tx_spend(&self, _args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { unimplemented!() } fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { MmError::err(TxMarshalingErr::NotSupported( diff --git a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs index 9224ea997f..4a5d13c05f 100644 --- a/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs +++ b/mm2src/coins/tendermint/rpc/tendermint_wasm_rpc.rs @@ -138,7 +138,7 @@ mod tests { #[wasm_bindgen_test] async fn test_get_abci_info() { - let client = HttpClient::new("https://rpc.sentry-02.theta-testnet.polypore.xyz", None).unwrap(); + let client = HttpClient::new("http://34.80.202.172:26657", None).unwrap(); client.abci_info().await.unwrap(); } } diff --git a/mm2src/coins/tendermint/tendermint_coin.rs b/mm2src/coins/tendermint/tendermint_coin.rs index 076c6c0a81..3b1cec75bb 100644 --- a/mm2src/coins/tendermint/tendermint_coin.rs +++ b/mm2src/coins/tendermint/tendermint_coin.rs @@ -6,6 +6,7 @@ use super::ibc::IBC_GAS_LIMIT_DEFAULT; use super::{rpc::*, TENDERMINT_COIN_PROTOCOL_TYPE}; use crate::coin_errors::{MyAddressError, ValidatePaymentError, ValidatePaymentResult}; use crate::hd_wallet::{HDPathAccountToAddressId, WithdrawFrom}; +use crate::rpc_command::tendermint::staking::ValidatorStatus; use crate::rpc_command::tendermint::{IBCChainRegistriesResponse, IBCChainRegistriesResult, IBCChainsRequestError, IBCTransferChannel, IBCTransferChannelTag, IBCTransferChannelsRequestError, IBCTransferChannelsResponse, IBCTransferChannelsResult, CHAIN_REGISTRY_BRANCH, @@ -35,18 +36,22 @@ use bitcrypto::{dhash160, sha256}; use common::executor::{abortable_queue::AbortableQueue, AbortableSystem}; use common::executor::{AbortedError, Timer}; use common::log::{debug, warn}; -use common::{get_utc_timestamp, now_sec, Future01CompatExt, DEX_FEE_ADDR_PUBKEY}; +use common::{get_utc_timestamp, now_sec, Future01CompatExt, PagingOptions, DEX_FEE_ADDR_PUBKEY}; use cosmrs::bank::{MsgMultiSend, MsgSend, MultiSendIo}; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::proto::cosmos::auth::v1beta1::{BaseAccount, QueryAccountRequest, QueryAccountResponse}; use cosmrs::proto::cosmos::bank::v1beta1::{MsgMultiSend as MsgMultiSendProto, MsgSend as MsgSendProto, QueryBalanceRequest, QueryBalanceResponse}; +use cosmrs::proto::cosmos::base::query::v1beta1::PageRequest; use cosmrs::proto::cosmos::base::tendermint::v1beta1::{GetBlockByHeightRequest, GetBlockByHeightResponse, GetLatestBlockRequest, GetLatestBlockResponse}; use cosmrs::proto::cosmos::base::v1beta1::Coin as CoinProto; +use cosmrs::proto::cosmos::staking::v1beta1::{QueryValidatorsRequest, + QueryValidatorsResponse as QueryValidatorsResponseProto}; use cosmrs::proto::cosmos::tx::v1beta1::{GetTxRequest, GetTxResponse, GetTxsEventRequest, GetTxsEventResponse, SimulateRequest, SimulateResponse, Tx, TxBody, TxRaw}; use cosmrs::proto::prost::{DecodeError, Message}; +use cosmrs::staking::{QueryValidatorsResponse, Validator}; use cosmrs::tendermint::block::Height; use cosmrs::tendermint::chain::Id as ChainId; use cosmrs::tendermint::PublicKey; @@ -93,6 +98,7 @@ const ABCI_QUERY_ACCOUNT_PATH: &str = "/cosmos.auth.v1beta1.Query/Account"; const ABCI_QUERY_BALANCE_PATH: &str = "/cosmos.bank.v1beta1.Query/Balance"; const ABCI_GET_TX_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTx"; const ABCI_GET_TXS_EVENT_PATH: &str = "/cosmos.tx.v1beta1.Service/GetTxsEvent"; +const ABCI_VALIDATORS_PATH: &str = "/cosmos.staking.v1beta1.Query/Validators"; pub(crate) const MIN_TX_SATOSHIS: i64 = 1; @@ -427,6 +433,8 @@ pub enum TendermintInitErrorKind { CantUseWatchersWithPubkeyPolicy, } +/// TODO: Rename this into `ClientRpcError` because this is very +/// confusing atm. #[derive(Display, Debug, Serialize, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] pub enum TendermintCoinRpcError { @@ -458,8 +466,9 @@ impl From for BalanceError { match err { TendermintCoinRpcError::InvalidResponse(e) => BalanceError::InvalidResponse(e), TendermintCoinRpcError::Prost(e) => BalanceError::InvalidResponse(e), - TendermintCoinRpcError::PerformError(e) => BalanceError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => BalanceError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + BalanceError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => BalanceError::Internal(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { BalanceError::Internal(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -473,8 +482,9 @@ impl From for ValidatePaymentError { match err { TendermintCoinRpcError::InvalidResponse(e) => ValidatePaymentError::InvalidRpcResponse(e), TendermintCoinRpcError::Prost(e) => ValidatePaymentError::InvalidRpcResponse(e), - TendermintCoinRpcError::PerformError(e) => ValidatePaymentError::Transport(e), - TendermintCoinRpcError::RpcClientError(e) => ValidatePaymentError::Transport(e), + TendermintCoinRpcError::PerformError(e) | TendermintCoinRpcError::RpcClientError(e) => { + ValidatePaymentError::Transport(e) + }, TendermintCoinRpcError::InternalError(e) => ValidatePaymentError::InternalError(e), TendermintCoinRpcError::UnexpectedAccountType { prefix } => { ValidatePaymentError::InvalidParameter(format!("Account type '{prefix}' is not supported for HTLCs")) @@ -2245,6 +2255,40 @@ impl TendermintCoin { None } + + pub(crate) async fn validators_list( + &self, + filter_status: ValidatorStatus, + paging: PagingOptions, + ) -> MmResult, TendermintCoinRpcError> { + let request = QueryValidatorsRequest { + status: filter_status.to_string(), + pagination: Some(PageRequest { + key: vec![], + offset: ((paging.page_number.get() - 1usize) * paging.limit) as u64, + limit: paging.limit as u64, + count_total: false, + reverse: false, + }), + }; + + let raw_response = self + .rpc_client() + .await? + .abci_query( + Some(ABCI_VALIDATORS_PATH.to_owned()), + request.encode_to_vec(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE, + ) + .await?; + + let decoded_proto = QueryValidatorsResponseProto::decode(raw_response.value.as_slice())?; + let typed_response = QueryValidatorsResponse::try_from(decoded_proto) + .map_err(|e| TendermintCoinRpcError::InternalError(e.to_string()))?; + + Ok(typed_response.validators) + } } fn clients_from_urls(ctx: &MmArc, nodes: Vec) -> MmResult, TendermintInitErrorKind> { @@ -2327,6 +2371,10 @@ impl MmCoin for TendermintCoin { fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); wallet_only_conf || self.is_keplr_from_ledger @@ -2761,14 +2809,14 @@ impl MarketCoinOps for TendermintCoin { Box::new(fut.boxed().compat()) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - let tx = try_tx_fus!(cosmrs::Tx::from_bytes(args.tx_bytes)); - let first_message = try_tx_fus!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); - let htlc_proto = try_tx_fus!(CreateHtlcProto::decode( - try_tx_fus!(HtlcType::from_str(&self.account_prefix)), + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + let tx = try_tx_s!(cosmrs::Tx::from_bytes(args.tx_bytes)); + let first_message = try_tx_s!(tx.body.messages.first().ok_or("Tx body couldn't be read.")); + let htlc_proto = try_tx_s!(CreateHtlcProto::decode( + try_tx_s!(HtlcType::from_str(&self.account_prefix)), first_message.value.as_slice() )); - let htlc = try_tx_fus!(CreateHtlcMsg::try_from(htlc_proto)); + let htlc = try_tx_s!(CreateHtlcMsg::try_from(htlc_proto)); let htlc_id = self.calculate_htlc_id(htlc.sender(), htlc.to(), htlc.amount(), args.secret_hash); let events_string = format!("claim_htlc.id='{}'", htlc_id); @@ -2783,38 +2831,32 @@ impl MarketCoinOps for TendermintCoin { }; let encoded_request = request.encode_to_vec(); - let coin = self.clone(); - let wait_until = args.wait_until; - let fut = async move { - loop { - let response = try_tx_s!( - try_tx_s!(coin.rpc_client().await) - .abci_query( - Some(ABCI_GET_TXS_EVENT_PATH.to_string()), - encoded_request.as_slice(), - ABCI_REQUEST_HEIGHT, - ABCI_REQUEST_PROVE - ) - .await - ); - let response = try_tx_s!(GetTxsEventResponse::decode(response.value.as_slice())); - if let Some(tx) = response.txs.first() { - return Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { - data: TxRaw { - body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), - signatures: tx.signatures.clone(), - }, - })); - } - Timer::sleep(5.).await; - if get_utc_timestamp() > wait_until as i64 { - return Err(TransactionErr::Plain("Waited too long".into())); - } + loop { + let response = try_tx_s!( + try_tx_s!(self.rpc_client().await) + .abci_query( + Some(ABCI_GET_TXS_EVENT_PATH.to_string()), + encoded_request.as_slice(), + ABCI_REQUEST_HEIGHT, + ABCI_REQUEST_PROVE + ) + .await + ); + let response = try_tx_s!(GetTxsEventResponse::decode(response.value.as_slice())); + if let Some(tx) = response.txs.first() { + return Ok(TransactionEnum::CosmosTransaction(CosmosTransaction { + data: TxRaw { + body_bytes: tx.body.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + auth_info_bytes: tx.auth_info.as_ref().map(Message::encode_to_vec).unwrap_or_default(), + signatures: tx.signatures.clone(), + }, + })); } - }; - - Box::new(fut.boxed().compat()) + Timer::sleep(5.).await; + if get_utc_timestamp() > args.wait_until as i64 { + return Err(TransactionErr::Plain("Waited too long".into())); + } + } } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { @@ -3810,18 +3852,15 @@ pub mod tendermint_coin_tests { let encoded_tx = tx.encode_to_vec(); let secret_hash = hex::decode("0C34C71EBA2A51738699F9F3D6DAFFB15BE576E8ED543203485791B5DA39D10D").unwrap(); - let spend_tx = block_on( - coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { - tx_bytes: &encoded_tx, - secret_hash: &secret_hash, - wait_until: get_utc_timestamp() as u64, - from_block: 0, - swap_contract_address: &None, - check_every: TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, - watcher_reward: false, - }) - .compat(), - ) + let spend_tx = block_on(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + tx_bytes: &encoded_tx, + secret_hash: &secret_hash, + wait_until: get_utc_timestamp() as u64, + from_block: 0, + swap_contract_address: &None, + check_every: TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, + watcher_reward: false, + })) .unwrap(); // https://nyancat.iobscan.io/#/tx?txHash=565C820C1F95556ADC251F16244AAD4E4274772F41BC13F958C9C2F89A14D137 diff --git a/mm2src/coins/tendermint/tendermint_token.rs b/mm2src/coins/tendermint/tendermint_token.rs index 5329a4a1f3..0e0ae9ceab 100644 --- a/mm2src/coins/tendermint/tendermint_token.rs +++ b/mm2src/coins/tendermint/tendermint_token.rs @@ -445,16 +445,18 @@ impl MarketCoinOps for TendermintToken { self.platform_coin.wait_for_confirmations(input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { - self.platform_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { - tx_bytes: args.tx_bytes, - secret_hash: args.secret_hash, - wait_until: args.wait_until, - from_block: args.from_block, - swap_contract_address: args.swap_contract_address, - check_every: args.check_every, - watcher_reward: false, - }) + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { + self.platform_coin + .wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + tx_bytes: args.tx_bytes, + secret_hash: args.secret_hash, + wait_until: args.wait_until, + from_block: args.from_block, + swap_contract_address: args.swap_contract_address, + check_every: args.check_every, + watcher_reward: false, + }) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { @@ -484,6 +486,10 @@ impl MmCoin for TendermintToken { fn wallet_only(&self, ctx: &MmArc) -> bool { let coin_conf = crate::coin_conf(ctx, self.ticker()); + // If coin is not in config, it means that it was added manually (a custom token) and should be treated as wallet only + if coin_conf.is_null() { + return true; + } let wallet_only_conf = coin_conf["wallet_only"].as_bool().unwrap_or(false); wallet_only_conf || self.platform_coin.is_keplr_from_ledger diff --git a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs index 1c8adf8de3..3dc95e7443 100644 --- a/mm2src/coins/tendermint/tendermint_tx_history_v2.rs +++ b/mm2src/coins/tendermint/tendermint_tx_history_v2.rs @@ -28,10 +28,24 @@ use std::convert::Infallible; const TX_PAGE_SIZE: u8 = 50; const DEFAULT_TRANSFER_EVENT_COUNT: usize = 1; + +const TRANSFER_EVENT: &str = "transfer"; + const CREATE_HTLC_EVENT: &str = "create_htlc"; const CLAIM_HTLC_EVENT: &str = "claim_htlc"; -const TRANSFER_EVENT: &str = "transfer"; -const ACCEPTED_EVENTS: &[&str] = &[CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT, TRANSFER_EVENT]; + +const IBC_SEND_EVENT: &str = "ibc_transfer"; +const IBC_RECEIVE_EVENT: &str = "fungible_token_packet"; +const IBC_NFT_RECEIVE_EVENT: &str = "non_fungible_token_packet"; + +const ACCEPTED_EVENTS: &[&str] = &[ + TRANSFER_EVENT, + CREATE_HTLC_EVENT, + CLAIM_HTLC_EVENT, + IBC_SEND_EVENT, + IBC_RECEIVE_EVENT, + IBC_NFT_RECEIVE_EVENT, +]; const RECEIVER_TAG_KEY: &str = "receiver"; const RECEIVER_TAG_KEY_BASE64: &str = "cmVjZWl2ZXI="; @@ -387,6 +401,8 @@ where Standard, CreateHtlc, ClaimHtlc, + IBCSend, + IBCReceive, } #[derive(Clone)] @@ -398,7 +414,28 @@ where transfer_event_type: TransferEventType, } - // updates sender and receiver addresses if tx is htlc, and if not leaves as it is. + /// Reads sender and receiver addresses properly from an IBC event. + fn read_real_ibc_addresses(transfer_details: &mut TransferDetails, msg_event: &Event) { + transfer_details.transfer_event_type = match msg_event.kind.as_str() { + IBC_SEND_EVENT => TransferEventType::IBCSend, + IBC_RECEIVE_EVENT | IBC_NFT_RECEIVE_EVENT => TransferEventType::IBCReceive, + _ => unreachable!("`read_real_ibc_addresses` shouldn't be called for non-IBC events."), + }; + + transfer_details.from = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + SENDER_TAG_KEY, + SENDER_TAG_KEY_BASE64 + )); + + transfer_details.to = some_or_return!(get_value_from_event_attributes( + &msg_event.attributes, + RECEIVER_TAG_KEY, + RECEIVER_TAG_KEY_BASE64, + )); + } + + /// Reads sender and receiver addresses properly from an HTLC event. fn read_real_htlc_addresses(transfer_details: &mut TransferDetails, msg_event: &&Event) { match msg_event.kind.as_str() { CREATE_HTLC_EVENT => { @@ -425,14 +462,14 @@ where transfer_details.transfer_event_type = TransferEventType::ClaimHtlc; }, - _ => {}, + _ => unreachable!("`read_real_htlc_addresses` shouldn't be called for non-HTLC events."), } } fn parse_transfer_values_from_events(tx_events: Vec<&Event>) -> Vec { let mut transfer_details_list: Vec = vec![]; - for (index, event) in tx_events.iter().enumerate() { + for event in tx_events.iter() { if event.kind.as_str() == TRANSFER_EVENT { let amount_with_denoms = some_or_continue!(get_value_from_event_attributes( &event.attributes, @@ -469,14 +506,20 @@ where transfer_event_type: TransferEventType::default(), }; - if index != 0 { - // If previous message is htlc related, that means current transfer - // addresses will be wrong. - if let Some(prev_event) = tx_events.get(index - 1) { - if [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&prev_event.kind.as_str()) { - read_real_htlc_addresses(&mut tx_details, prev_event); - } - }; + // For HTLC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_htlc_addresses` to handle them properly. + if let Some(htlc_event) = tx_events + .iter() + .find(|e| [CREATE_HTLC_EVENT, CLAIM_HTLC_EVENT].contains(&e.kind.as_str())) + { + read_real_htlc_addresses(&mut tx_details, htlc_event); + } + // For IBC transactions, the sender and receiver addresses in the "transfer" event will be incorrect. + // Use `read_real_ibc_addresses` to handle them properly. + else if let Some(ibc_event) = tx_events.iter().find(|e| { + [IBC_SEND_EVENT, IBC_RECEIVE_EVENT, IBC_NFT_RECEIVE_EVENT].contains(&e.kind.as_str()) + }) { + read_real_ibc_addresses(&mut tx_details, ibc_event); } // sum the amounts coins and pairs are same @@ -561,7 +604,7 @@ where } }, TransferEventType::ClaimHtlc => Some((vec![my_address], vec![])), - TransferEventType::Standard => { + TransferEventType::Standard | TransferEventType::IBCSend | TransferEventType::IBCReceive => { Some((vec![transfer_details.from.clone()], vec![transfer_details.to.clone()])) }, } diff --git a/mm2src/coins/test_coin.rs b/mm2src/coins/test_coin.rs index f03767bf5b..c6c8b7fccf 100644 --- a/mm2src/coins/test_coin.rs +++ b/mm2src/coins/test_coin.rs @@ -2,7 +2,7 @@ use super::{CoinBalance, CommonSwapOpsV2, FundingTxSpend, HistorySyncState, MarketCoinOps, MmCoin, RawTransactionFut, RawTransactionRequest, RefundTakerPaymentArgs, SearchForFundingSpendErr, SwapOps, TradeFee, - TransactionEnum, TransactionFut, WaitForTakerPaymentSpendError}; + TransactionEnum, TransactionFut, WaitForPaymentSpendError}; use crate::coin_errors::ValidatePaymentResult; use crate::{coin_errors::MyAddressError, BalanceFut, CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinFutSpawner, ConfirmPaymentInput, FeeApproxStage, FoundSwapTxSpend, GenPreimageResult, GenTakerFundingSpendArgs, @@ -92,7 +92,7 @@ impl MarketCoinOps for TestCoin { unimplemented!() } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { unimplemented!() } + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { unimplemented!() } fn tx_enum_from_bytes(&self, _bytes: &[u8]) -> Result> { MmError::err(TxMarshalingErr::NotSupported( @@ -566,7 +566,7 @@ impl TakerCoinSwapOpsV2 for TestCoin { taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { unimplemented!() } } diff --git a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs index cf0575f973..f5b1312a65 100644 --- a/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs +++ b/mm2src/coins/tx_history_storage/sql_tx_history_storage_v2.rs @@ -376,11 +376,12 @@ pub struct SqliteTxHistoryStorage(Arc>); impl SqliteTxHistoryStorage { pub fn new(ctx: &MmArc) -> Result> { - let sqlite_connection = ctx - .sqlite_connection - .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( - "sqlite_connection is not initialized".to_owned(), - )))?; + let sqlite_connection = + ctx.sqlite_connection + .get() + .ok_or(MmError::new(CreateTxHistoryStorageError::Internal( + "sqlite_connection is not initialized".to_owned(), + )))?; Ok(SqliteTxHistoryStorage(sqlite_connection.clone())) } } diff --git a/mm2src/coins/utxo/bch.rs b/mm2src/coins/utxo/bch.rs index 43054c6241..a9daf60854 100644 --- a/mm2src/coins/utxo/bch.rs +++ b/mm2src/coins/utxo/bch.rs @@ -1242,7 +1242,7 @@ impl MarketCoinOps for BchCoin { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { utxo_common::wait_for_output_spend( self.clone(), args.tx_bytes, @@ -1251,6 +1251,7 @@ impl MarketCoinOps for BchCoin { args.wait_until, args.check_every, ) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/utxo/qtum.rs b/mm2src/coins/utxo/qtum.rs index fa7d0c37a6..cda04d69bf 100644 --- a/mm2src/coins/utxo/qtum.rs +++ b/mm2src/coins/utxo/qtum.rs @@ -863,7 +863,7 @@ impl MarketCoinOps for QtumCoin { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { utxo_common::wait_for_output_spend( self.clone(), args.tx_bytes, @@ -872,6 +872,7 @@ impl MarketCoinOps for QtumCoin { args.wait_until, args.check_every, ) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/utxo/slp.rs b/mm2src/coins/utxo/slp.rs index 6667123ed5..7eb24f64a0 100644 --- a/mm2src/coins/utxo/slp.rs +++ b/mm2src/coins/utxo/slp.rs @@ -1187,7 +1187,7 @@ impl MarketCoinOps for SlpToken { self.platform_coin.wait_for_confirmations(input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { utxo_common::wait_for_output_spend( self.clone(), args.tx_bytes, @@ -1196,6 +1196,7 @@ impl MarketCoinOps for SlpToken { args.wait_until, args.check_every, ) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs index 89266af2f6..c065016176 100644 --- a/mm2src/coins/utxo/utxo_block_header_storage/mod.rs +++ b/mm2src/coins/utxo/utxo_block_header_storage/mod.rs @@ -27,7 +27,7 @@ impl Debug for BlockHeaderStorage { impl BlockHeaderStorage { #[cfg(all(not(test), not(target_arch = "wasm32")))] pub(crate) fn new_from_ctx(ctx: MmArc, ticker: String) -> Result { - let sqlite_connection = ctx.sqlite_connection.ok_or(BlockHeaderStorageError::Internal( + let sqlite_connection = ctx.sqlite_connection.get().ok_or(BlockHeaderStorageError::Internal( "sqlite_connection is not initialized".to_owned(), ))?; Ok(BlockHeaderStorage { @@ -50,8 +50,11 @@ impl BlockHeaderStorage { use db_common::sqlite::rusqlite::Connection; use std::sync::{Arc, Mutex}; - let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap())); - let conn = ctx.sqlite_connection.clone_or(conn); + let conn = ctx + .sqlite_connection + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); Ok(BlockHeaderStorage { inner: Box::new(SqliteBlockHeadersStorage { ticker, conn }), diff --git a/mm2src/coins/utxo/utxo_common.rs b/mm2src/coins/utxo/utxo_common.rs index 1b686a88ed..5b48ac279c 100644 --- a/mm2src/coins/utxo/utxo_common.rs +++ b/mm2src/coins/utxo/utxo_common.rs @@ -2159,7 +2159,7 @@ pub fn watcher_validate_taker_fee( if tx_confirmed_before_block { return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( "{}: Fee tx {:?} confirmed before min_block {}", - EARLY_CONFIRMATION_ERR_LOG, taker_fee_tx, min_block_number + EARLY_CONFIRMATION_ERR_LOG, tx_from_rpc, min_block_number ))); } @@ -3024,24 +3024,21 @@ pub async fn wait_for_output_spend_impl( } } -pub fn wait_for_output_spend + Send + Sync + 'static>( +pub async fn wait_for_output_spend + Send + Sync + 'static>( coin: T, tx_bytes: &[u8], output_index: usize, from_block: u64, wait_until: u64, check_every: f64, -) -> TransactionFut { - let mut tx: UtxoTx = try_tx_fus!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); +) -> TransactionResult { + let mut tx: UtxoTx = try_tx_s!(deserialize(tx_bytes).map_err(|e| ERRL!("{:?}", e))); tx.tx_hash_algo = coin.as_ref().tx_hash_algo; - let fut = async move { - wait_for_output_spend_impl(coin.as_ref(), &tx, output_index, from_block, wait_until, check_every) - .await - .map(|tx| tx.into()) - .map_err(|e| TransactionErr::Plain(format!("{:?}", e))) - }; - Box::new(fut.boxed().compat()) + wait_for_output_spend_impl(coin.as_ref(), &tx, output_index, from_block, wait_until, check_every) + .await + .map(|tx| tx.into()) + .map_err(|e| TransactionErr::Plain(format!("{:?}", e))) } pub fn tx_enum_from_bytes(coin: &UtxoCoinFields, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/utxo/utxo_standard.rs b/mm2src/coins/utxo/utxo_standard.rs index 5f2864e2b6..962f276b10 100644 --- a/mm2src/coins/utxo/utxo_standard.rs +++ b/mm2src/coins/utxo/utxo_standard.rs @@ -37,7 +37,7 @@ use crate::{CanRefundHtlc, CheckIfMyPaymentSentArgs, CoinBalance, CoinBalanceMap ValidateMakerPaymentArgs, ValidateOtherPubKeyErr, ValidatePaymentError, ValidatePaymentFut, ValidatePaymentInput, ValidateSwapV2TxResult, ValidateTakerFundingArgs, ValidateTakerFundingSpendPreimageResult, ValidateTakerPaymentSpendPreimageResult, - ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForTakerPaymentSpendError, + ValidateWatcherSpendInput, VerificationResult, WaitForHTLCTxSpendArgs, WaitForPaymentSpendError, WatcherOps, WatcherReward, WatcherRewardError, WatcherSearchForSwapTxSpendInput, WatcherValidatePaymentInput, WatcherValidateTakerFeeInput, WithdrawFut}; use common::executor::{AbortableSystem, AbortedError}; @@ -864,7 +864,7 @@ impl TakerCoinSwapOpsV2 for UtxoStandardCoin { taker_payment: &Self::Tx, from_block: u64, wait_until: u64, - ) -> MmResult { + ) -> MmResult { let res = utxo_common::wait_for_output_spend_impl( self.as_ref(), taker_payment, @@ -937,7 +937,7 @@ impl MarketCoinOps for UtxoStandardCoin { utxo_common::wait_for_confirmations(&self.utxo_arc, input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { utxo_common::wait_for_output_spend( self.clone(), args.tx_bytes, @@ -946,6 +946,7 @@ impl MarketCoinOps for UtxoStandardCoin { args.wait_until, args.check_every, ) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/utxo/utxo_tests.rs b/mm2src/coins/utxo/utxo_tests.rs index 9d0bbe5b01..b6c316aee2 100644 --- a/mm2src/coins/utxo/utxo_tests.rs +++ b/mm2src/coins/utxo/utxo_tests.rs @@ -438,7 +438,7 @@ fn test_wait_for_payment_spend_timeout_native() { let wait_until = now_sec() - 1; let from_block = 1000; - assert!(block_on_f01(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + assert!(block_on(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &transaction, secret_hash: &[], wait_until, @@ -493,7 +493,7 @@ fn test_wait_for_payment_spend_timeout_electrum() { let wait_until = now_sec() - 1; let from_block = 1000; - assert!(block_on_f01(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + assert!(block_on(coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &transaction, secret_hash: &[], wait_until, diff --git a/mm2src/coins/z_coin.rs b/mm2src/coins/z_coin.rs index 9e247fe17b..bf47ff9e90 100644 --- a/mm2src/coins/z_coin.rs +++ b/mm2src/coins/z_coin.rs @@ -140,7 +140,7 @@ cfg_native!( const SAPLING_OUTPUT_EXPECTED_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4"; ); -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ZcoinConsensusParams { // we don't support coins without overwinter and sapling active so these are mandatory overwinter_activation_height: u32, @@ -157,7 +157,7 @@ pub struct ZcoinConsensusParams { b58_script_address_prefix: [u8; 2], } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct CheckPointBlockInfo { height: u32, hash: H256Json, @@ -165,7 +165,7 @@ pub struct CheckPointBlockInfo { sapling_tree: BytesJson, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] pub struct ZcoinProtocolInfo { consensus_params: ZcoinConsensusParams, check_point_block: Option, @@ -1217,7 +1217,7 @@ impl MarketCoinOps for ZCoin { utxo_common::wait_for_confirmations(self.as_ref(), input) } - fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionFut { + async fn wait_for_htlc_tx_spend(&self, args: WaitForHTLCTxSpendArgs<'_>) -> TransactionResult { utxo_common::wait_for_output_spend( self.clone(), args.tx_bytes, @@ -1226,6 +1226,7 @@ impl MarketCoinOps for ZCoin { args.wait_until, args.check_every, ) + .await } fn tx_enum_from_bytes(&self, bytes: &[u8]) -> Result> { diff --git a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs index 7523360807..fea9a93277 100644 --- a/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs +++ b/mm2src/coins/z_coin/storage/blockdb/blockdb_sql_storage.rs @@ -49,10 +49,9 @@ impl BlockDbImpl { async_blocking(move || { let conn = Connection::open(path).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; let conn = Arc::new(Mutex::new(conn)); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -61,6 +60,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(Self { db: conn, ticker }) }) @@ -73,11 +73,12 @@ impl BlockDbImpl { async_blocking(move || { let conn = ctx .sqlite_connection - .clone_or(Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); - let conn_clone = conn.clone(); - let conn_clone = conn_clone.lock().unwrap(); - run_optimization_pragmas(&conn_clone).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; - conn_clone + .get() + .cloned() + .unwrap_or_else(|| Arc::new(Mutex::new(Connection::open_in_memory().unwrap()))); + let conn_lock = conn.lock().unwrap(); + run_optimization_pragmas(&conn_lock).map_err(|err| ZcoinStorageError::DbError(err.to_string()))?; + conn_lock .execute( "CREATE TABLE IF NOT EXISTS compactblocks ( height INTEGER PRIMARY KEY, @@ -86,6 +87,7 @@ impl BlockDbImpl { [], ) .map_to_mm(|err| ZcoinStorageError::DbError(err.to_string()))?; + drop(conn_lock); Ok(BlockDbImpl { db: conn, ticker }) }) diff --git a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs index 9dbaf389a3..d9ac5ec322 100644 --- a/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs +++ b/mm2src/coins/z_coin/storage/walletdb/wasm/storage.rs @@ -808,7 +808,7 @@ impl WalletRead for WalletIndexedDb { let locked_db = self.lock_db().await?; let db_transaction = locked_db.get_inner().transaction().await?; let block_headers_db = db_transaction.table::().await?; - let earlist_block = block_headers_db + let earliest_block = block_headers_db .cursor_builder() .only("ticker", &self.ticker)? .bound("height", 0u32, u32::MAX) @@ -830,7 +830,7 @@ impl WalletRead for WalletIndexedDb { .next() .await?; - if let (Some(min), Some(max)) = (earlist_block, latest_block) { + if let (Some(min), Some(max)) = (earliest_block, latest_block) { Ok(Some((BlockHeight::from(min.1.height), BlockHeight::from(max.1.height)))) } else { Ok(None) diff --git a/mm2src/coins_activation/src/erc20_token_activation.rs b/mm2src/coins_activation/src/erc20_token_activation.rs index 0f7f4edbd9..77970284b4 100644 --- a/mm2src/coins_activation/src/erc20_token_activation.rs +++ b/mm2src/coins_activation/src/erc20_token_activation.rs @@ -10,6 +10,7 @@ use coins::{eth::{v2_activation::{Erc20Protocol, EthTokenActivationError}, use common::Future01CompatExt; use mm2_err_handle::prelude::*; use serde::Serialize; +use serde_json::Value as Json; use std::collections::HashMap; #[derive(Debug, Serialize)] @@ -43,6 +44,7 @@ impl From for EnableTokenError { EthTokenActivationError::InvalidPayload(e) => EnableTokenError::InvalidPayload(e), EthTokenActivationError::UnexpectedDerivationMethod(e) => EnableTokenError::UnexpectedDerivationMethod(e), EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => EnableTokenError::PrivKeyPolicyNotAllowed(e), + EthTokenActivationError::CustomTokenError(e) => EnableTokenError::CustomTokenError(e), } } } @@ -133,13 +135,21 @@ impl TokenActivationOps for EthCoin { ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + token_conf: Json, protocol_conf: Self::ProtocolInfo, + is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { match activation_params { EthTokenActivationParams::Erc20(erc20_init_params) => match protocol_conf { EthTokenProtocol::Erc20(erc20_protocol) => { let token = platform_coin - .initialize_erc20_token(erc20_init_params, erc20_protocol, ticker.clone()) + .initialize_erc20_token( + ticker.clone(), + erc20_init_params, + token_conf, + erc20_protocol, + is_custom, + ) .await?; let address = display_eth_address(&token.derivation_method().single_addr_or_err().await?); diff --git a/mm2src/coins_activation/src/eth_with_token_activation.rs b/mm2src/coins_activation/src/eth_with_token_activation.rs index 62e8fe4c4c..7bc62b444a 100644 --- a/mm2src/coins_activation/src/eth_with_token_activation.rs +++ b/mm2src/coins_activation/src/eth_with_token_activation.rs @@ -12,7 +12,7 @@ use coins::coin_balance::{CoinBalanceReport, EnableCoinBalanceOps}; use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, Erc20Protocol, Erc20TokenActivationRequest, EthActivationV2Error, EthActivationV2Request, EthPrivKeyActivationPolicy}; use coins::eth::v2_activation::{EthTokenActivationError, NftActivationRequest, NftProviderEnum}; -use coins::eth::{display_eth_address, Erc20TokenInfo, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; +use coins::eth::{display_eth_address, Erc20TokenDetails, EthCoin, EthCoinType, EthPrivKeyBuildPolicy}; use coins::hd_wallet::RpcTaskXPubExtractor; use coins::my_tx_history_v2::TxHistoryStorage; use coins::nft::nft_structs::NftInfo; @@ -85,6 +85,7 @@ impl From for EnablePlatformCoinWithTokensError { EthActivationV2Error::InvalidHardwareWalletCall => EnablePlatformCoinWithTokensError::Internal( "Hardware wallet must be used within rpc task manager".to_string(), ), + EthActivationV2Error::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -118,6 +119,7 @@ impl From for InitTokensAsMmCoinsError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) }, EthTokenActivationError::PrivKeyPolicyNotAllowed(e) => InitTokensAsMmCoinsError::Internal(e.to_string()), + EthTokenActivationError::CustomTokenError(e) => InitTokensAsMmCoinsError::CustomTokenError(e), } } } @@ -143,7 +145,13 @@ impl TokenInitializer for Erc20Initializer { for param in activation_params { let token: EthCoin = self .platform_coin - .initialize_erc20_token(param.activation_request, param.protocol, param.ticker) + .initialize_erc20_token( + param.ticker, + param.activation_request, + param.conf, + param.protocol, + param.is_custom, + ) .await?; tokens.push(token); } @@ -183,7 +191,7 @@ impl RegisterTokenInfo for EthCoin { return; } - self.add_erc_token_info(token.ticker().to_string(), Erc20TokenInfo { + self.add_erc_token_info(token.ticker().to_string(), Erc20TokenDetails { token_address: token.erc20_token_address().unwrap(), decimals: token.decimals(), }); diff --git a/mm2src/coins_activation/src/init_erc20_token_activation.rs b/mm2src/coins_activation/src/init_erc20_token_activation.rs index de322c9ee5..f162cb1754 100644 --- a/mm2src/coins_activation/src/init_erc20_token_activation.rs +++ b/mm2src/coins_activation/src/init_erc20_token_activation.rs @@ -7,7 +7,7 @@ use coins::coin_balance::{EnableCoinBalanceError, EnableCoinBalanceOps}; use coins::eth::v2_activation::{Erc20Protocol, EthTokenActivationError, InitErc20TokenActivationRequest}; use coins::eth::EthCoin; use coins::hd_wallet::RpcTaskXPubExtractor; -use coins::{MarketCoinOps, MmCoin, RegisterCoinError}; +use coins::{CustomTokenError, MarketCoinOps, MmCoin, RegisterCoinError}; use common::Future01CompatExt; use crypto::HwRpcError; use derive_more::Display; @@ -17,6 +17,7 @@ use mm2_err_handle::prelude::*; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type Erc20TokenTaskManagerShared = InitTokenTaskManagerShared; @@ -38,6 +39,8 @@ pub enum InitErc20Error { Transport(String), #[display(fmt = "Internal error: {}", _0)] Internal(String), + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for InitTokenError { @@ -45,13 +48,16 @@ impl From for InitTokenError { match e { InitErc20Error::HwError(hw) => InitTokenError::HwError(hw), InitErc20Error::TaskTimedOut { duration } => InitTokenError::TaskTimedOut { duration }, - InitErc20Error::TokenIsAlreadyActivated { ticker } => InitTokenError::TokenIsAlreadyActivated { ticker }, + InitErc20Error::TokenIsAlreadyActivated { ticker, .. } => { + InitTokenError::TokenIsAlreadyActivated { ticker } + }, InitErc20Error::TokenCreationError { ticker, error } => { InitTokenError::TokenCreationError { ticker, error } }, InitErc20Error::CouldNotFetchBalance(error) => InitTokenError::CouldNotFetchBalance(error), InitErc20Error::Transport(transport) => InitTokenError::Transport(transport), InitErc20Error::Internal(internal) => InitTokenError::Internal(internal), + InitErc20Error::CustomTokenError(error) => InitTokenError::CustomTokenError(error), } } } @@ -66,6 +72,7 @@ impl From for InitErc20Error { | EthTokenActivationError::CouldNotFetchBalance(_) | EthTokenActivationError::InvalidPayload(_) | EthTokenActivationError::Transport(_) => InitErc20Error::Transport(e.to_string()), + EthTokenActivationError::CustomTokenError(e) => InitErc20Error::CustomTokenError(e), } } } @@ -118,11 +125,19 @@ impl InitTokenActivationOps for EthCoin { ticker: String, platform_coin: Self::PlatformCoin, activation_request: &Self::ActivationRequest, + token_conf: Json, protocol_conf: Self::ProtocolInfo, _task_handle: InitTokenTaskHandleShared, + is_custom: bool, ) -> Result> { let token = platform_coin - .initialize_erc20_token(activation_request.clone().into(), protocol_conf, ticker) + .initialize_erc20_token( + ticker, + activation_request.clone().into(), + token_conf, + protocol_conf, + is_custom, + ) .await?; Ok(token) diff --git a/mm2src/coins_activation/src/init_token.rs b/mm2src/coins_activation/src/init_token.rs index dbc03b1754..01d47b3656 100644 --- a/mm2src/coins_activation/src/init_token.rs +++ b/mm2src/coins_activation/src/init_token.rs @@ -5,7 +5,8 @@ use crate::prelude::{coin_conf_with_protocol, CoinConfWithProtocolError, Current use crate::token::TokenProtocolParams; use async_trait::async_trait; use coins::coin_balance::CoinBalanceReport; -use coins::{lp_coinfind, lp_coinfind_or_err, CoinBalanceMap, CoinProtocol, CoinsContext, MmCoinEnum, RegisterCoinError}; +use coins::{lp_coinfind, lp_coinfind_or_err, CoinBalanceMap, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, + RegisterCoinError}; use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::HwRpcError; @@ -19,6 +20,7 @@ use rpc_task::{RpcTask, RpcTaskError, RpcTaskHandleShared, RpcTaskManager, RpcTa RpcTaskTypes, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; use std::time::Duration; pub type InitTokenResponse = InitRpcTaskResponse; @@ -37,6 +39,7 @@ pub type CancelInitTokenError = CancelRpcTaskError; #[derive(Debug, Deserialize, Clone)] pub struct InitTokenReq { ticker: String, + protocol: Option, activation_params: T, } @@ -65,8 +68,10 @@ pub trait InitTokenActivationOps: Into + TokenOf + Clone + Send + Sy ticker: String, platform_coin: Self::PlatformCoin, activation_request: &Self::ActivationRequest, + token_conf: Json, protocol_conf: Self::ProtocolInfo, task_handle: InitTokenTaskHandleShared, + is_custom: bool, ) -> Result>; /// Returns the result of the token activation. @@ -94,7 +99,8 @@ where return MmError::err(InitTokenError::TokenIsAlreadyActivated { ticker: request.ticker }); } - let (_, token_protocol): (_, Token::ProtocolInfo) = coin_conf_with_protocol(&ctx, &request.ticker)?; + let (token_conf, token_protocol): (_, Token::ProtocolInfo) = + coin_conf_with_protocol(&ctx, &request.ticker, request.protocol.clone())?; let platform_coin = lp_coinfind_or_err(&ctx, token_protocol.platform_coin_ticker()) .await @@ -111,6 +117,7 @@ where let task = InitTokenTask:: { ctx, request, + token_conf, token_protocol, platform_coin, }; @@ -174,6 +181,7 @@ pub async fn cancel_init_token( pub struct InitTokenTask { ctx: MmArc, request: InitTokenReq, + token_conf: Json, token_protocol: Token::ProtocolInfo, platform_coin: Token::PlatformCoin, } @@ -210,8 +218,10 @@ where ticker.clone(), self.platform_coin.clone(), &self.request.activation_params, + self.token_conf.clone(), self.token_protocol.clone(), task_handle.clone(), + self.request.protocol.is_some(), ) .await?; @@ -297,8 +307,8 @@ pub enum InitTokenError { TokenConfigIsNotFound(String), #[display(fmt = "Token {} protocol parsing failed: {}", ticker, error)] TokenProtocolParseError { ticker: String, error: String }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] - UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] + UnexpectedTokenProtocol { ticker: String, protocol: Json }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] TokenCreationError { ticker: String, error: String }, #[display(fmt = "Could not fetch balance: {}", _0)] @@ -310,6 +320,8 @@ pub enum InitTokenError { platform_coin_ticker: String, token_ticker: String, }, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), #[display(fmt = "{}", _0)] HwError(HwRpcError), #[display(fmt = "Transport error: {}", _0)] @@ -331,6 +343,7 @@ impl From for InitTokenError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitTokenError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitTokenError::CustomTokenError(e), } } } @@ -354,7 +367,8 @@ impl HttpStatusCode for InitTokenError { | InitTokenError::TokenProtocolParseError { .. } | InitTokenError::UnexpectedTokenProtocol { .. } | InitTokenError::TokenCreationError { .. } - | InitTokenError::PlatformCoinIsNotActivated(_) => StatusCode::BAD_REQUEST, + | InitTokenError::PlatformCoinIsNotActivated(_) + | InitTokenError::CustomTokenError(_) => StatusCode::BAD_REQUEST, InitTokenError::TaskTimedOut { .. } => StatusCode::REQUEST_TIMEOUT, InitTokenError::HwError(_) => StatusCode::GONE, InitTokenError::CouldNotFetchBalance(_) diff --git a/mm2src/coins_activation/src/l2/init_l2.rs b/mm2src/coins_activation/src/l2/init_l2.rs index 20e66ebbde..e6b0888700 100644 --- a/mm2src/coins_activation/src/l2/init_l2.rs +++ b/mm2src/coins_activation/src/l2/init_l2.rs @@ -79,7 +79,7 @@ where return MmError::err(InitL2Error::L2IsAlreadyActivated(ticker)); } - let (coin_conf_json, protocol_conf): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &ticker)?; + let (coin_conf_json, protocol_conf): (Json, L2::ProtocolInfo) = coin_conf_with_protocol(&ctx, &ticker, None)?; let coin_conf = L2::coin_conf_from_json(coin_conf_json)?; let platform_coin = lp_coinfind_or_err(&ctx, protocol_conf.platform_coin_ticker()) diff --git a/mm2src/coins_activation/src/l2/init_l2_error.rs b/mm2src/coins_activation/src/l2/init_l2_error.rs index d23fd73078..71eb57fd23 100644 --- a/mm2src/coins_activation/src/l2/init_l2_error.rs +++ b/mm2src/coins_activation/src/l2/init_l2_error.rs @@ -1,11 +1,11 @@ use crate::prelude::CoinConfWithProtocolError; -use coins::CoinProtocol; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserActionError}; use rpc_task::RpcTaskError; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type InitL2StatusError = RpcTaskStatusError; @@ -24,10 +24,10 @@ pub enum InitL2Error { ticker: String, error: String, }, - #[display(fmt = "Unexpected layer 2 protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected layer 2 protocol {} for {}", protocol, ticker)] UnexpectedL2Protocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Platform coin {} is not activated", _0)] PlatformCoinIsNotActivated(String), @@ -62,6 +62,9 @@ impl From for InitL2Error { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitL2Error::UnexpectedL2Protocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => { + InitL2Error::Internal(format!("Custom tokens are not supported for L2: {}", e)) + }, } } } diff --git a/mm2src/coins_activation/src/platform_coin_with_tokens.rs b/mm2src/coins_activation/src/platform_coin_with_tokens.rs index 051dd22fc3..d5ee5cfbf0 100644 --- a/mm2src/coins_activation/src/platform_coin_with_tokens.rs +++ b/mm2src/coins_activation/src/platform_coin_with_tokens.rs @@ -5,8 +5,8 @@ use crate::prelude::*; use async_trait::async_trait; use coins::my_tx_history_v2::TxHistoryStorage; use coins::tx_history_storage::{CreateTxHistoryStorageError, TxHistoryStorageBuilder}; -use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, MmCoinEnum, PrivKeyPolicyNotAllowed, - UnexpectedDerivationMethod}; +use coins::{lp_coinfind, lp_coinfind_any, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, + PrivKeyPolicyNotAllowed, UnexpectedDerivationMethod}; use common::{log, HttpStatusCode, StatusCode, SuccessResponse}; use crypto::hw_rpc_task::{HwConnectStatuses, HwRpcTaskAwaitingStatus, HwRpcTaskUserAction}; use crypto::CryptoCtxError; @@ -38,6 +38,7 @@ pub type InitPlatformCoinWithTokensTaskManagerShared = #[derive(Clone, Debug, Deserialize)] pub struct TokenActivationRequest { ticker: String, + protocol: Option, #[serde(flatten)] request: Req, } @@ -51,8 +52,10 @@ pub trait TokenOf: Into { pub struct TokenActivationParams { pub(crate) ticker: String, + pub(crate) conf: Json, pub(crate) activation_request: Req, pub(crate) protocol: Protocol, + pub(crate) is_custom: bool, } #[async_trait] @@ -87,15 +90,15 @@ pub trait TokenAsMmCoinInitializer: Send + Sync { } pub enum InitTokensAsMmCoinsError { - TokenAlreadyActivated(String), TokenConfigIsNotFound(String), CouldNotFetchBalance(String), UnexpectedDerivationMethod(UnexpectedDerivationMethod), Internal(String), TokenProtocolParseError { ticker: String, error: String }, - UnexpectedTokenProtocol { ticker: String, protocol: CoinProtocol }, + UnexpectedTokenProtocol { ticker: String, protocol: Json }, Transport(String), InvalidPayload(String), + CustomTokenError(CustomTokenError), } impl From for InitTokensAsMmCoinsError { @@ -111,6 +114,7 @@ impl From for InitTokensAsMmCoinsError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitTokensAsMmCoinsError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitTokensAsMmCoinsError::CustomTokenError(e), } } } @@ -138,11 +142,14 @@ where let token_params = tokens_requests .into_iter() .map(|req| -> Result<_, MmError> { - let (_, protocol): (_, T::TokenProtocol) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (token_conf, protocol): (_, T::TokenProtocol) = + coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; Ok(TokenActivationParams { ticker: req.ticker, + conf: token_conf, activation_request: req.request, protocol, + is_custom: req.protocol.is_some(), }) }) .collect::, _>>()?; @@ -235,7 +242,6 @@ pub struct EnablePlatformCoinWithTokensReq { #[serde(tag = "error_type", content = "error_data")] pub enum EnablePlatformCoinWithTokensError { PlatformIsAlreadyActivated(String), - TokenIsAlreadyActivated(String), #[display(fmt = "Platform {} config is not found", _0)] PlatformConfigIsNotFound(String), #[display(fmt = "Platform coin {} protocol parsing failed: {}", ticker, error)] @@ -243,10 +249,10 @@ pub enum EnablePlatformCoinWithTokensError { ticker: String, error: String, }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] UnexpectedPlatformProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Token {} config is not found", _0)] TokenConfigIsNotFound(String), @@ -255,10 +261,10 @@ pub enum EnablePlatformCoinWithTokensError { ticker: String, error: String, }, - #[display(fmt = "Unexpected token protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected token protocol {} for {}", protocol, ticker)] UnexpectedTokenProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] PlatformCoinCreationError { @@ -283,6 +289,8 @@ pub enum EnablePlatformCoinWithTokensError { }, #[display(fmt = "Hardware policy must be activated within task manager")] UnexpectedDeviceActivationPolicy, + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EnablePlatformCoinWithTokensError { @@ -300,6 +308,7 @@ impl From for EnablePlatformCoinWithTokensError { error: err.to_string(), } }, + CoinConfWithProtocolError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -307,9 +316,6 @@ impl From for EnablePlatformCoinWithTokensError { impl From for EnablePlatformCoinWithTokensError { fn from(err: InitTokensAsMmCoinsError) -> Self { match err { - InitTokensAsMmCoinsError::TokenAlreadyActivated(ticker) => { - EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(ticker) - }, InitTokensAsMmCoinsError::TokenConfigIsNotFound(ticker) => { EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(ticker) }, @@ -327,6 +333,7 @@ impl From for EnablePlatformCoinWithTokensError { InitTokensAsMmCoinsError::UnexpectedDerivationMethod(e) => { EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(e.to_string()) }, + InitTokensAsMmCoinsError::CustomTokenError(e) => EnablePlatformCoinWithTokensError::CustomTokenError(e), } } } @@ -362,9 +369,9 @@ impl HttpStatusCode for EnablePlatformCoinWithTokensError { | EnablePlatformCoinWithTokensError::PrivKeyPolicyNotAllowed(_) | EnablePlatformCoinWithTokensError::UnexpectedDerivationMethod(_) | EnablePlatformCoinWithTokensError::Internal(_) - | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } => StatusCode::INTERNAL_SERVER_ERROR, + | EnablePlatformCoinWithTokensError::TaskTimedOut { .. } + | EnablePlatformCoinWithTokensError::CustomTokenError(_) => StatusCode::INTERNAL_SERVER_ERROR, EnablePlatformCoinWithTokensError::PlatformIsAlreadyActivated(_) - | EnablePlatformCoinWithTokensError::TokenIsAlreadyActivated(_) | EnablePlatformCoinWithTokensError::PlatformConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::TokenConfigIsNotFound(_) | EnablePlatformCoinWithTokensError::UnexpectedPlatformProtocol { .. } @@ -449,7 +456,7 @@ where )); } - let (platform_conf, platform_protocol) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (platform_conf, platform_protocol) = coin_conf_with_protocol(&ctx, &req.ticker, None)?; let platform_coin = Platform::enable_platform_coin( ctx.clone(), diff --git a/mm2src/coins_activation/src/prelude.rs b/mm2src/coins_activation/src/prelude.rs index d000170fa3..42c93c1377 100644 --- a/mm2src/coins_activation/src/prelude.rs +++ b/mm2src/coins_activation/src/prelude.rs @@ -2,12 +2,12 @@ use coins::siacoin::SiaCoinActivationParams; use coins::utxo::UtxoActivationParams; use coins::z_coin::ZcoinActivationParams; -use coins::{coin_conf, CoinBalance, CoinProtocol, DerivationMethodResponse, MmCoinEnum}; +use coins::{coin_conf, CoinBalance, CoinProtocol, CustomTokenError, DerivationMethodResponse, MmCoinEnum}; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use mm2_number::BigDecimal; use serde_derive::Serialize; -use serde_json::{self as json, Value as Json}; +use serde_json::{self as json, json, Value as Json}; use std::collections::{HashMap, HashSet}; pub trait CurrentBlock { @@ -64,31 +64,78 @@ pub trait TryFromCoinProtocol { pub enum CoinConfWithProtocolError { ConfigIsNotFound(String), CoinProtocolParseError { ticker: String, err: json::Error }, - UnexpectedProtocol { ticker: String, protocol: CoinProtocol }, + UnexpectedProtocol { ticker: String, protocol: Json }, + CustomTokenError(CustomTokenError), } /// Determines the coin configuration and protocol information for a given coin or NFT ticker. -/// In the case of NFT ticker, it's platform coin config will be returned. -#[allow(clippy::result_large_err)] pub fn coin_conf_with_protocol( ctx: &MmArc, coin: &str, + protocol_from_request: Option, ) -> Result<(Json, T), MmError> { let conf = coin_conf(ctx, coin); - if conf.is_null() { - return MmError::err(CoinConfWithProtocolError::ConfigIsNotFound(coin.into())); + let is_ticker_in_config = !conf.is_null(); + + // For `protocol_from_request`: None = config-based activation, Some = custom token activation + match (protocol_from_request, is_ticker_in_config) { + // Config-based activation requested with an existing configuration + // Proceed with parsing protocol info from config + (None, true) => parse_coin_protocol_from_config(conf, coin), + // Custom token activation requested and no matching ticker in config + // Proceed with custom token config creation from protocol info + (Some(protocol), false) => create_custom_token_config(ctx, coin, protocol), + // Custom token activation requested but a coin with the same ticker already exists in config + (Some(_), true) => Err(MmError::new(CoinConfWithProtocolError::CustomTokenError( + CustomTokenError::DuplicateTickerInConfig { + ticker_in_config: coin.to_string(), + }, + ))), + // Config-based activation requested but ticker not found in config + (None, false) => Err(MmError::new(CoinConfWithProtocolError::ConfigIsNotFound(coin.into()))), } - let coin_protocol: CoinProtocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { +} + +fn parse_coin_protocol_from_config( + conf: Json, + coin: &str, +) -> Result<(Json, T), MmError> { + let protocol = json::from_value(conf["protocol"].clone()).map_to_mm(|err| { CoinConfWithProtocolError::CoinProtocolParseError { ticker: coin.into(), err, } })?; + let coin_protocol = - T::try_from_coin_protocol(coin_protocol).mm_err(|protocol| CoinConfWithProtocolError::UnexpectedProtocol { + T::try_from_coin_protocol(protocol).mm_err(|p| CoinConfWithProtocolError::UnexpectedProtocol { ticker: coin.into(), - protocol, + protocol: json!(p), })?; + + Ok((conf, coin_protocol)) +} + +fn create_custom_token_config( + ctx: &MmArc, + coin: &str, + protocol: CoinProtocol, +) -> Result<(Json, T), MmError> { + protocol + .custom_token_validations(ctx) + .mm_err(CoinConfWithProtocolError::CustomTokenError)?; + + let conf = json!({ + "protocol": protocol, + "wallet_only": true + }); + + let coin_protocol = + T::try_from_coin_protocol(protocol).mm_err(|p| CoinConfWithProtocolError::UnexpectedProtocol { + ticker: coin.into(), + protocol: json!(p), + })?; + Ok((conf, coin_protocol)) } diff --git a/mm2src/coins_activation/src/slp_token_activation.rs b/mm2src/coins_activation/src/slp_token_activation.rs index f9abc166f4..91dbf95ea7 100644 --- a/mm2src/coins_activation/src/slp_token_activation.rs +++ b/mm2src/coins_activation/src/slp_token_activation.rs @@ -7,6 +7,7 @@ use coins::{CoinBalance, CoinProtocol, MarketCoinOps, MmCoin, MmCoinEnum}; use mm2_err_handle::prelude::*; use rpc::v1::types::H256 as H256Json; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; use std::collections::HashMap; impl TryPlatformCoinFromMmCoinEnum for BchCoin { @@ -82,7 +83,9 @@ impl TokenActivationOps for SlpToken { ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + _token_conf: Json, protocol_conf: Self::ProtocolInfo, + _is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { // confirmation settings from activation params have the highest priority let required_confirmations = activation_params.required_confirmations.unwrap_or_else(|| { diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs index 314f3066b4..a90f53e968 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin.rs @@ -90,7 +90,7 @@ where return MmError::err(InitStandaloneCoinError::CoinIsAlreadyActivated { ticker: request.ticker }); } - let (coin_conf, protocol_info) = coin_conf_with_protocol(&ctx, &request.ticker)?; + let (coin_conf, protocol_info) = coin_conf_with_protocol(&ctx, &request.ticker, None)?; let coins_act_ctx = CoinsActivationContext::from_ctx(&ctx).map_to_mm(InitStandaloneCoinError::Internal)?; let spawner = ctx.spawner(); diff --git a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs index 1f0b5db764..21b1d696a9 100644 --- a/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs +++ b/mm2src/coins_activation/src/standalone_coin/init_standalone_coin_error.rs @@ -1,5 +1,4 @@ use crate::prelude::CoinConfWithProtocolError; -use coins::CoinProtocol; use common::{HttpStatusCode, StatusCode}; use crypto::HwRpcError; use derive_more::Display; @@ -7,6 +6,7 @@ use rpc_task::rpc_common::{CancelRpcTaskError, RpcTaskStatusError, RpcTaskUserAc use rpc_task::{RpcTaskError, TaskId}; use ser_error_derive::SerializeErrorType; use serde_derive::Serialize; +use serde_json::Value as Json; use std::time::Duration; pub type InitStandaloneCoinStatusError = RpcTaskStatusError; @@ -26,8 +26,8 @@ pub enum InitStandaloneCoinError { CoinConfigIsNotFound(String), #[display(fmt = "Coin {} protocol parsing failed: {}", ticker, error)] CoinProtocolParseError { ticker: String, error: String }, - #[display(fmt = "Unexpected platform protocol {:?} for {}", protocol, ticker)] - UnexpectedCoinProtocol { ticker: String, protocol: CoinProtocol }, + #[display(fmt = "Unexpected platform protocol {} for {}", protocol, ticker)] + UnexpectedCoinProtocol { ticker: String, protocol: Json }, #[display(fmt = "Error on platform coin {} creation: {}", ticker, error)] CoinCreationError { ticker: String, error: String }, #[display(fmt = "{}", _0)] @@ -51,6 +51,10 @@ impl From for InitStandaloneCoinError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { InitStandaloneCoinError::UnexpectedCoinProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => InitStandaloneCoinError::Internal(format!( + "Custom tokens are not supported for standalone coins: {}", + e + )), } } } diff --git a/mm2src/coins_activation/src/tendermint_token_activation.rs b/mm2src/coins_activation/src/tendermint_token_activation.rs index 29598969af..12808505a9 100644 --- a/mm2src/coins_activation/src/tendermint_token_activation.rs +++ b/mm2src/coins_activation/src/tendermint_token_activation.rs @@ -7,6 +7,7 @@ use coins::{tendermint::{TendermintCoin, TendermintToken, TendermintTokenActivat use common::Future01CompatExt; use mm2_err_handle::prelude::{MapMmError, MmError}; use serde::Serialize; +use serde_json::Value as Json; use std::collections::HashMap; impl From for EnableTokenError { @@ -54,7 +55,9 @@ impl TokenActivationOps for TendermintToken { ticker: String, platform_coin: Self::PlatformCoin, _activation_params: Self::ActivationParams, + _token_conf: Json, protocol_conf: Self::ProtocolInfo, + _is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError> { let token = TendermintToken::new(ticker, platform_coin, protocol_conf.decimals, protocol_conf.denom)?; diff --git a/mm2src/coins_activation/src/token.rs b/mm2src/coins_activation/src/token.rs index 0493c68fdb..5001a0f3de 100644 --- a/mm2src/coins_activation/src/token.rs +++ b/mm2src/coins_activation/src/token.rs @@ -4,7 +4,7 @@ use crate::platform_coin_with_tokens::{self, RegisterTokenInfo}; use crate::prelude::*; use async_trait::async_trait; use coins::utxo::rpc_clients::UtxoRpcError; -use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, MmCoinEnum, +use coins::{lp_coinfind, lp_coinfind_or_err, BalanceError, CoinProtocol, CoinsContext, CustomTokenError, MmCoinEnum, PrivKeyPolicyNotAllowed, RegisterCoinError, UnexpectedDerivationMethod}; use common::{HttpStatusCode, StatusCode}; use derive_more::Display; @@ -12,6 +12,7 @@ use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::*; use ser_error_derive::SerializeErrorType; use serde_derive::{Deserialize, Serialize}; +use serde_json::Value as Json; pub trait TokenProtocolParams { fn platform_coin_ticker(&self) -> &str; @@ -28,7 +29,9 @@ pub trait TokenActivationOps: Into + platform_coin_with_tokens::Toke ticker: String, platform_coin: Self::PlatformCoin, activation_params: Self::ActivationParams, + token_conf: Json, protocol_conf: Self::ProtocolInfo, + is_custom: bool, ) -> Result<(Self, Self::ActivationResult), MmError>; } @@ -44,10 +47,10 @@ pub enum EnableTokenError { ticker: String, error: String, }, - #[display(fmt = "Unexpected token protocol {:?} for {}", protocol, ticker)] + #[display(fmt = "Unexpected token protocol {} for {}", protocol, ticker)] UnexpectedTokenProtocol { ticker: String, - protocol: CoinProtocol, + protocol: Json, }, #[display(fmt = "Platform coin {} is not activated", _0)] PlatformCoinIsNotActivated(String), @@ -64,6 +67,8 @@ pub enum EnableTokenError { Internal(String), InvalidPayload(String), PrivKeyPolicyNotAllowed(PrivKeyPolicyNotAllowed), + #[display(fmt = "Custom token error: {}", _0)] + CustomTokenError(CustomTokenError), } impl From for EnableTokenError { @@ -88,6 +93,7 @@ impl From for EnableTokenError { CoinConfWithProtocolError::UnexpectedProtocol { ticker, protocol } => { EnableTokenError::UnexpectedTokenProtocol { ticker, protocol } }, + CoinConfWithProtocolError::CustomTokenError(e) => EnableTokenError::CustomTokenError(e), } } } @@ -105,6 +111,7 @@ impl From for EnableTokenError { #[derive(Debug, Deserialize)] pub struct EnableTokenRequest { ticker: String, + protocol: Option, activation_params: T, } @@ -121,7 +128,8 @@ where return MmError::err(EnableTokenError::TokenIsAlreadyActivated(req.ticker)); } - let (_, token_protocol): (_, Token::ProtocolInfo) = coin_conf_with_protocol(&ctx, &req.ticker)?; + let (token_conf, token_protocol): (_, Token::ProtocolInfo) = + coin_conf_with_protocol(&ctx, &req.ticker, req.protocol.clone())?; let platform_coin = lp_coinfind_or_err(&ctx, token_protocol.platform_coin_ticker()) .await @@ -134,8 +142,15 @@ where } })?; - let (token, activation_result) = - Token::enable_token(req.ticker, platform_coin.clone(), req.activation_params, token_protocol).await?; + let (token, activation_result) = Token::enable_token( + req.ticker, + platform_coin.clone(), + req.activation_params, + token_conf, + token_protocol, + req.protocol.is_some(), + ) + .await?; let coins_ctx = CoinsContext::from_ctx(&ctx).unwrap(); coins_ctx.add_token(token.clone().into()).await?; @@ -164,7 +179,8 @@ impl HttpStatusCode for EnableTokenError { | EnableTokenError::PlatformCoinIsNotActivated(_) | EnableTokenError::TokenConfigIsNotFound { .. } | EnableTokenError::UnexpectedTokenProtocol { .. } - | EnableTokenError::InvalidPayload(_) => StatusCode::BAD_REQUEST, + | EnableTokenError::InvalidPayload(_) + | EnableTokenError::CustomTokenError(_) => StatusCode::BAD_REQUEST, EnableTokenError::TokenProtocolParseError { .. } | EnableTokenError::UnsupportedPlatformCoin { .. } | EnableTokenError::UnexpectedDerivationMethod(_) diff --git a/mm2src/common/Cargo.toml b/mm2src/common/Cargo.toml index 6a5395b360..7f5b4803ed 100644 --- a/mm2src/common/Cargo.toml +++ b/mm2src/common/Cargo.toml @@ -10,6 +10,7 @@ path = "common.rs" doctest = false [features] +for-tests = [] track-ctx-pointer = ["shared_ref_counter/enable", "shared_ref_counter/log"] [dependencies] @@ -34,9 +35,10 @@ lazy_static = "1.4" log = "0.4.17" parking_lot = { version = "0.12.0", features = ["nightly"] } parking_lot_core = { version = "0.6", features = ["nightly"] } +paste = "1.0" primitive-types = "0.11.1" rand = { version = "0.7", features = ["std", "small_rng"] } -rustc-hash = "1.1.0" +rustc-hash = "2.0" regex = "1" serde = "1" serde_derive = "1" diff --git a/mm2src/common/common.rs b/mm2src/common/common.rs index 7e3abe470e..1b540f8154 100644 --- a/mm2src/common/common.rs +++ b/mm2src/common/common.rs @@ -152,6 +152,7 @@ use futures01::{future, Future}; use http::header::CONTENT_TYPE; use http::Response; use parking_lot::{Mutex as PaMutex, MutexGuard as PaMutexGuard}; +pub use paste::paste; use rand::RngCore; use rand::{rngs::SmallRng, SeedableRng}; use serde::{de, ser}; @@ -161,14 +162,14 @@ use std::convert::TryInto; use std::fmt::Write as FmtWrite; use std::fs::File; use std::future::Future as Future03; -use std::io::{BufReader, Read, Write}; +use std::io::{self, BufReader, Read, Write}; use std::iter::Peekable; use std::mem::{forget, zeroed}; use std::num::{NonZeroUsize, TryFromIntError}; use std::ops::{Add, Deref, Div, RangeInclusive}; use std::os::raw::c_void; use std::panic::{set_hook, PanicInfo}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::ptr::read_volatile; use std::sync::atomic::Ordering; use std::time::{Duration, SystemTime, SystemTimeError}; @@ -806,7 +807,7 @@ pub fn kdf_app_dir() -> Option { } /// Returns path of the coins file. -pub fn kdf_coins_file() -> PathBuf { +pub fn kdf_coins_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_COINS_PATH").ok(); @@ -817,7 +818,7 @@ pub fn kdf_coins_file() -> PathBuf { } /// Returns path of the config file. -pub fn kdf_config_file() -> PathBuf { +pub fn kdf_config_file() -> Result { #[cfg(not(target_arch = "wasm32"))] let value_from_env = env::var("MM_CONF_PATH").ok(); @@ -833,16 +834,41 @@ pub fn kdf_config_file() -> PathBuf { /// 1- From the environment variable. /// 2- From the current directory where app is called. /// 3- From the root application directory. -pub fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> PathBuf { +fn find_kdf_dependency_file(value_from_env: Option, path_leaf: &str) -> Result { if let Some(path) = value_from_env { - return PathBuf::from(path); + let path = PathBuf::from(path); + require_file(&path)?; + return Ok(path); } let from_current_dir = PathBuf::from(path_leaf); - if from_current_dir.exists() { + + let path = if from_current_dir.exists() { from_current_dir } else { kdf_app_dir().unwrap_or_default().join(path_leaf) + }; + + require_file(&path)?; + return Ok(path); + + fn require_file(path: &Path) -> Result<(), io::Error> { + if path.is_dir() { + // TODO: use `IsADirectory` variant which is stabilized with 1.83 + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Expected file but '{}' is a directory.", path.display()), + )); + } + + if !path.exists() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + format!("File '{}' is not present.", path.display()), + )); + } + + Ok(()) } } @@ -1069,7 +1095,17 @@ impl Default for PagingOptionsEnum { } #[inline(always)] -pub fn get_utc_timestamp() -> i64 { Utc::now().timestamp() } +pub fn get_utc_timestamp() -> i64 { + // get_utc_timestamp for tests allowing to add some bias to 'now' + #[cfg(feature = "for-tests")] + return Utc::now().timestamp() + + std::env::var("TEST_TIMESTAMP_OFFSET") + .map(|s| s.as_str().parse::().unwrap_or_default()) + .unwrap_or_default(); + + #[cfg(not(feature = "for-tests"))] + return Utc::now().timestamp(); +} #[inline(always)] pub fn get_utc_timestamp_nanos() -> i64 { Utc::now().timestamp_nanos() } @@ -1144,6 +1180,37 @@ pub fn http_uri_to_ws_address(uri: http::Uri) -> String { format!("{}{}{}{}", address_prefix, host_address, port, path) } +/// If 0x prefix exists in an str strip it or return the str as-is +#[macro_export] +macro_rules! str_strip_0x { + ($s: expr) => { + $s.strip_prefix("0x").unwrap_or($s) + }; +} + +/// If value is 'some' push key and value (as string) into an array containing (key, value) elements +#[macro_export] +macro_rules! push_if_some { + ($arr: expr, $k: expr, $v: expr) => { + if let Some(v) = $v { + $arr.push(($k, v.to_string())) + } + }; +} + +/// Define 'with_...' method to set a parameter with an optional value in a builder +#[macro_export] +macro_rules! def_with_opt_param { + ($var: ident, $var_type: ty) => { + $crate::paste! { + pub fn [](&mut self, $var: Option<$var_type>) -> &mut Self { + self.$var = $var; + self + } + } + }; +} + #[test] fn test_http_uri_to_ws_address() { let uri = "https://cosmos-rpc.polkachu.com".parse::().unwrap(); diff --git a/mm2src/crypto/src/crypto_ctx.rs b/mm2src/crypto/src/crypto_ctx.rs index 92ac1f2196..ffc83603a6 100644 --- a/mm2src/crypto/src/crypto_ctx.rs +++ b/mm2src/crypto/src/crypto_ctx.rs @@ -316,10 +316,12 @@ impl CryptoCtx { *ctx_field = Some(result.clone()); drop(ctx_field); - ctx.rmd160.pin(rmd160).map_to_mm(CryptoInitError::Internal)?; + ctx.rmd160 + .set(rmd160) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; ctx.shared_db_id - .pin(shared_db_id) - .map_to_mm(CryptoInitError::Internal)?; + .set(shared_db_id) + .map_to_mm(|_| CryptoInitError::Internal("Already Initialized".to_string()))?; info!("Public key hash: {rmd160}"); info!("Shared Database ID: {shared_db_id}"); diff --git a/mm2src/crypto/src/mnemonic.rs b/mm2src/crypto/src/mnemonic.rs index c92e23c05b..922b292da0 100644 --- a/mm2src/crypto/src/mnemonic.rs +++ b/mm2src/crypto/src/mnemonic.rs @@ -104,7 +104,7 @@ pub fn encrypt_mnemonic(mnemonic: &str, password: &str) -> MmResult MmResult { +pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmResult { // Re-create the salts from Base64-encoded strings let (salt_aes, salt_hmac) = match &encrypted_data.key_derivation_details { KeyDerivationDetails::Argon2 { @@ -126,8 +126,7 @@ pub fn decrypt_mnemonic(encrypted_data: &EncryptedData, password: &str) -> MmRes // Convert decrypted data back to a string let mnemonic_str = String::from_utf8(decrypted_data).map_to_mm(|e| MnemonicError::DecodeError(e.to_string()))?; - let mnemonic = Mnemonic::parse_normalized(&mnemonic_str)?; - Ok(mnemonic) + Ok(mnemonic_str) } #[cfg(any(test, target_arch = "wasm32"))] @@ -144,10 +143,23 @@ mod tests { let mnemonic = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; let password = "password"; - // Verify that the mnemonic is valid - let parsed_mnemonic = Mnemonic::parse_normalized(mnemonic); - assert!(parsed_mnemonic.is_ok()); - let parsed_mnemonic = parsed_mnemonic.unwrap(); + // Encrypt the mnemonic + let encrypted_data = encrypt_mnemonic(mnemonic, password); + assert!(encrypted_data.is_ok()); + let encrypted_data = encrypted_data.unwrap(); + + // Decrypt the mnemonic + let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic.is_ok()); + let decrypted_mnemonic = decrypted_mnemonic.unwrap(); + + // Verify if decrypted mnemonic matches the original + assert_eq!(decrypted_mnemonic, mnemonic); + }); + + cross_test!(test_encrypt_decrypt_non_bip39_mnemonic, { + let mnemonic = "Helloworld"; + let password = "Helloworld"; // Encrypt the mnemonic let encrypted_data = encrypt_mnemonic(mnemonic, password); @@ -160,7 +172,7 @@ mod tests { let decrypted_mnemonic = decrypted_mnemonic.unwrap(); // Verify if decrypted mnemonic matches the original - assert_eq!(decrypted_mnemonic, parsed_mnemonic); + assert_eq!(decrypted_mnemonic, mnemonic); }); cross_test!(test_mnemonic_with_last_byte_zero, { @@ -173,7 +185,9 @@ mod tests { let encrypted_data = encrypted_data.unwrap(); // Decrypt the mnemonic - let decrypted_mnemonic = decrypt_mnemonic(&encrypted_data, password); + let decrypted_mnemonic_str = decrypt_mnemonic(&encrypted_data, password); + assert!(decrypted_mnemonic_str.is_ok()); + let decrypted_mnemonic = Mnemonic::parse_normalized(&decrypted_mnemonic_str.unwrap()); assert!(decrypted_mnemonic.is_err()); // Verify that the error is due to parsing and not padding diff --git a/mm2src/mm2_bin_lib/Cargo.toml b/mm2src/mm2_bin_lib/Cargo.toml index 7415f21b4f..3019fb6add 100644 --- a/mm2src/mm2_bin_lib/Cargo.toml +++ b/mm2src/mm2_bin_lib/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "mm2_bin_lib" -version = "2.2.0-beta" +version = "2.3.0-beta" authors = ["James Lee", "Artem Pikulin", "Artem Grinblat", "Omar S.", "Onur Ozkan", "Alina Sharon", "Caglar Kaya", "Cipi", "Sergey Boiko", "Samuel Onoja", "Roman Sztergbaum", "Kadan Stadelmann ", "Dimxy", "Omer Yacine"] edition = "2018" default-run = "kdf" @@ -15,6 +15,7 @@ custom-swap-locktime = ["mm2_main/custom-swap-locktime"] # only for testing purp native = ["mm2_main/native"] # Deprecated track-ctx-pointer = ["mm2_main/track-ctx-pointer"] zhtlc-native-tests = ["mm2_main/zhtlc-native-tests"] +test-ext-api = ["mm2_main/test-ext-api"] [[bin]] name = "mm2" diff --git a/mm2src/mm2_bin_lib/src/lib.rs b/mm2src/mm2_bin_lib/src/lib.rs index c78233e64a..7ac292aa63 100644 --- a/mm2src/mm2_bin_lib/src/lib.rs +++ b/mm2src/mm2_bin_lib/src/lib.rs @@ -41,7 +41,7 @@ fn mm2_status() -> MainStatus { Err(_) => return MainStatus::NoRpc, }; - if ctx.rpc_started.copy_or(false) { + if *ctx.rpc_started.get().unwrap_or(&false) { MainStatus::RpcIsUp } else { MainStatus::NoRpc diff --git a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs index 94ed6daf62..17d7a839bc 100644 --- a/mm2src/mm2_bin_lib/src/mm2_native_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_native_lib.rs @@ -123,7 +123,7 @@ pub extern "C" fn mm2_test(torch: i32, log_cb: extern "C" fn(line: *const c_char }, }; let conf = json::to_string(&ctx.conf).unwrap(); - let hy_res = mm2_main::rpc::lp_commands_legacy::stop(ctx); + let hy_res = mm2_main::rpc::lp_commands::legacy::stop(ctx); let r = match block_on(hy_res) { Ok(r) => r, Err(err) => { diff --git a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs index ee56bd4045..f878e1b914 100644 --- a/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs +++ b/mm2src/mm2_bin_lib/src/mm2_wasm_lib.rs @@ -216,7 +216,7 @@ pub async fn mm2_rpc(payload: JsValue) -> Result { Err(_) => return Err(Mm2RpcErr::NotRunning.into()), }; - let wasm_rpc = ctx.wasm_rpc.ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; + let wasm_rpc = ctx.wasm_rpc.get().ok_or(JsValue::from(Mm2RpcErr::NotRunning))?; let response: Mm2RpcResponse = wasm_rpc.request(request_json).await.into(); serialize_to_js(&response).map_err(|e| { diff --git a/mm2src/mm2_bitcoin/rpc/src/lib.rs b/mm2src/mm2_bitcoin/rpc/src/lib.rs index d901dbdb22..70efe15dc7 100644 --- a/mm2src/mm2_bitcoin/rpc/src/lib.rs +++ b/mm2src/mm2_bitcoin/rpc/src/lib.rs @@ -1,4 +1,3 @@ -extern crate core; #[cfg(test)] extern crate lazy_static; extern crate log; extern crate rustc_hex as hex; diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs index 9ff3d3e36e..5368985a08 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/bytes.rs @@ -85,8 +85,8 @@ impl ops::Deref for Bytes { fn deref(&self) -> &Self::Target { &self.0 } } -impl ::core::fmt::LowerHex for Bytes { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { +impl ::std::fmt::LowerHex for Bytes { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } diff --git a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs index 1911d11781..00ecf4f52f 100644 --- a/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs +++ b/mm2src/mm2_bitcoin/rpc/src/v1/types/hash.rs @@ -147,8 +147,8 @@ macro_rules! impl_hash { } } - impl ::core::fmt::LowerHex for $name { - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + impl ::std::fmt::LowerHex for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { for i in &self.0[..] { write!(f, "{:02x}", i)?; } diff --git a/mm2src/mm2_core/Cargo.toml b/mm2src/mm2_core/Cargo.toml index d0df9dbe7c..365592bffe 100644 --- a/mm2src/mm2_core/Cargo.toml +++ b/mm2src/mm2_core/Cargo.toml @@ -15,9 +15,10 @@ common = { path = "../common" } db_common = { path = "../db_common" } derive_more = "0.99" futures = { version = "0.3", package = "futures", features = ["compat", "async-await", "thread-pool"] } +gstuff = { version = "0.7", features = ["nightly"] } hex = "0.4.2" lazy_static = "1.4" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_metrics = { path = "../mm2_metrics" } @@ -31,13 +32,11 @@ shared_ref_counter = { path = "../common/shared_ref_counter" } uuid = { version = "1.2.2", features = ["fast-rng", "serde", "v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -gstuff = { version = "0.7", features = ["nightly"] } instant = { version = "0.1.12", features = ["wasm-bindgen"] } mm2_rpc = { path = "../mm2_rpc", features = [ "rpc_facilities" ] } wasm-bindgen-test = { version = "0.3.2" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rustls = { version = "0.21", default-features = false } -gstuff = { version = "0.7", features = ["nightly"] } instant = "0.1.12" tokio = { version = "1.20", features = ["io-util", "rt-multi-thread", "net"] } diff --git a/mm2src/mm2_core/src/mm_ctx.rs b/mm2src/mm2_core/src/mm_ctx.rs index 8c417f2ce1..0a1afb2eea 100644 --- a/mm2src/mm2_core/src/mm_ctx.rs +++ b/mm2src/mm2_core/src/mm_ctx.rs @@ -7,7 +7,7 @@ use common::{executor::{abortable_queue::{AbortableQueue, WeakSpawner}, expirable_map::ExpirableMap}; use futures::channel::oneshot; use futures::lock::Mutex as AsyncMutex; -use gstuff::{try_s, Constructible, ERR, ERRL}; +use gstuff::{try_s, ERR, ERRL}; use lazy_static::lazy_static; use libp2p::PeerId; use mm2_event_stream::{controller::Controller, Event, EventStreamConfiguration}; @@ -22,7 +22,7 @@ use std::collections::HashSet; use std::fmt; use std::future::Future; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use crate::data_asker::DataAsker; @@ -76,9 +76,9 @@ pub struct MmCtx { /// Should be refactored away in the future. State should always be valid. /// If there are things that are loaded in background then they should be separately optional, /// without invalidating the entire state. - pub initialized: Constructible, + pub initialized: OnceLock, /// True if the RPC HTTP server was started. - pub rpc_started: Constructible, + pub rpc_started: OnceLock, /// Controller for continuously streaming data using streaming channels of `mm2_event_stream`. pub stream_channel_controller: Controller, /// Data transfer bridge between server and client where server (which is the mm2 runtime) initiates the request. @@ -86,10 +86,10 @@ pub struct MmCtx { /// Configuration of event streaming used for SSE. pub event_stream_configuration: Option, /// True if the MarketMaker instance needs to stop. - pub stop: Constructible, + pub stop: OnceLock, /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. /// 0 if the handler ID is allocated yet. - pub ffi_handle: Constructible, + pub ffi_handle: OnceLock, /// The context belonging to the `ordermatch` mod: `OrdermatchContext`. pub ordermatch_ctx: Mutex>>, pub rate_limit_ctx: Mutex>>, @@ -104,10 +104,10 @@ pub struct MmCtx { pub crypto_ctx: Mutex>>, /// RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from passphrase. /// This hash is **unique** among Iguana and each HD accounts derived from the same passphrase. - pub rmd160: Constructible, + pub rmd160: OnceLock, /// A shared DB identifier - RIPEMD160(SHA256(x)) where x is secp256k1 pubkey derived from (passphrase + magic salt). /// This hash is **the same** for Iguana and all HD accounts derived from the same passphrase. - pub shared_db_id: Constructible, + pub shared_db_id: OnceLock, /// Coins that should be enabled to kick start the interrupted swaps and orders. pub coins_needed_for_kick_start: Mutex>, /// The context belonging to the `lp_swap` mod: `SwapsContext`. @@ -115,19 +115,19 @@ pub struct MmCtx { /// The context belonging to the `lp_stats` mod: `StatsContext` pub stats_ctx: Mutex>>, /// Wallet name for this mm2 instance. Optional for backwards compatibility. - pub wallet_name: Constructible>, + pub wallet_name: OnceLock>, /// The context belonging to the `lp_wallet` mod: `WalletsContext`. #[cfg(target_arch = "wasm32")] pub wallets_ctx: Mutex>>, /// The RPC sender forwarding requests to writing part of underlying stream. #[cfg(target_arch = "wasm32")] - pub wasm_rpc: Constructible, + pub wasm_rpc: OnceLock, /// Deprecated, please use `async_sqlite_connection` for new implementations. #[cfg(not(target_arch = "wasm32"))] - pub sqlite_connection: Constructible>>, + pub sqlite_connection: OnceLock>>, /// Deprecated, please create `shared_async_sqlite_conn` for new implementations and call db `KOMODEFI-shared.db`. #[cfg(not(target_arch = "wasm32"))] - pub shared_sqlite_conn: Constructible>>, + pub shared_sqlite_conn: OnceLock>>, pub mm_version: String, pub datetime: String, pub mm_init_ctx: Mutex>>, @@ -144,7 +144,7 @@ pub struct MmCtx { pub nft_ctx: Mutex>>, /// asynchronous handle for rusqlite connection. #[cfg(not(target_arch = "wasm32"))] - pub async_sqlite_connection: Constructible>>, + pub async_sqlite_connection: OnceLock>>, /// Links the RPC context to the P2P context to handle health check responses. pub healthcheck_response_handler: AsyncMutex>>, } @@ -155,13 +155,13 @@ impl MmCtx { conf: Json::Object(json::Map::new()), log: log::LogArc::new(log), metrics: MetricsArc::new(), - initialized: Constructible::default(), - rpc_started: Constructible::default(), + initialized: OnceLock::default(), + rpc_started: OnceLock::default(), stream_channel_controller: Controller::new(), data_asker: DataAsker::default(), event_stream_configuration: None, - stop: Constructible::default(), - ffi_handle: Constructible::default(), + stop: OnceLock::default(), + ffi_handle: OnceLock::default(), ordermatch_ctx: Mutex::new(None), rate_limit_ctx: Mutex::new(None), simple_market_maker_bot_ctx: Mutex::new(None), @@ -172,20 +172,20 @@ impl MmCtx { coins_ctx: Mutex::new(None), coins_activation_ctx: Mutex::new(None), crypto_ctx: Mutex::new(None), - rmd160: Constructible::default(), - shared_db_id: Constructible::default(), + rmd160: OnceLock::default(), + shared_db_id: OnceLock::default(), coins_needed_for_kick_start: Mutex::new(HashSet::new()), swaps_ctx: Mutex::new(None), stats_ctx: Mutex::new(None), - wallet_name: Constructible::default(), + wallet_name: OnceLock::default(), #[cfg(target_arch = "wasm32")] wallets_ctx: Mutex::new(None), #[cfg(target_arch = "wasm32")] - wasm_rpc: Constructible::default(), + wasm_rpc: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - sqlite_connection: Constructible::default(), + sqlite_connection: OnceLock::default(), #[cfg(not(target_arch = "wasm32"))] - shared_sqlite_conn: Constructible::default(), + shared_sqlite_conn: OnceLock::default(), mm_version: "".into(), datetime: "".into(), mm_init_ctx: Mutex::new(None), @@ -195,7 +195,7 @@ impl MmCtx { db_namespace: DbNamespaceId::Main, nft_ctx: Mutex::new(None), #[cfg(not(target_arch = "wasm32"))] - async_sqlite_connection: Constructible::default(), + async_sqlite_connection: OnceLock::default(), healthcheck_response_handler: AsyncMutex::new(ExpirableMap::default()), } } @@ -204,14 +204,14 @@ impl MmCtx { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.rmd160.or(&|| &*DEFAULT) + self.rmd160.get().unwrap_or(&*DEFAULT) } pub fn shared_db_id(&self) -> &H160 { lazy_static! { static ref DEFAULT: H160 = [0; 20].into(); } - self.shared_db_id.or(&|| &*DEFAULT) + self.shared_db_id.get().unwrap_or(&*DEFAULT) } #[cfg(not(target_arch = "wasm32"))] @@ -346,7 +346,7 @@ impl MmCtx { pub fn spawner(&self) -> MmFutSpawner { MmFutSpawner::new(&self.abortable_system) } /// True if the MarketMaker instance needs to stop. - pub fn is_stopping(&self) -> bool { self.stop.copy_or(false) } + pub fn is_stopping(&self) -> bool { *self.stop.get().unwrap_or(&false) } pub fn gui(&self) -> Option<&str> { self.conf["gui"].as_str() } @@ -357,7 +357,10 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("MM2.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.sqlite_connection.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -366,7 +369,10 @@ impl MmCtx { let sqlite_file_path = self.shared_dbdir().join("MM2-shared.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let connection = try_s!(Connection::open(sqlite_file_path)); - try_s!(self.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection)))); + try_s!(self + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } @@ -375,19 +381,23 @@ impl MmCtx { let sqlite_file_path = self.dbdir().join("KOMODEFI.db"); log_sqlite_file_open_attempt(&sqlite_file_path); let async_conn = try_s!(AsyncConnection::open(sqlite_file_path).await); - try_s!(self.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(async_conn)))); + try_s!(self + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(async_conn))) + .map_err(|_| "Already initialized".to_string())); Ok(()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_conn_opt(&self) -> Option> { - self.sqlite_connection.as_option().map(|conn| conn.lock().unwrap()) + self.sqlite_connection.get().map(|conn| conn.lock().unwrap()) } #[cfg(not(target_arch = "wasm32"))] pub fn sqlite_connection(&self) -> MutexGuard { self.sqlite_connection - .or(&|| panic!("sqlite_connection is not initialized")) + .get() + .expect("sqlite_connection is not initialized") .lock() .unwrap() } @@ -395,7 +405,8 @@ impl MmCtx { #[cfg(not(target_arch = "wasm32"))] pub fn shared_sqlite_conn(&self) -> MutexGuard { self.shared_sqlite_conn - .or(&|| panic!("shared_sqlite_conn is not initialized")) + .get() + .expect("shared_sqlite_conn is not initialized") .lock() .unwrap() } @@ -409,7 +420,7 @@ impl Drop for MmCtx { fn drop(&mut self) { let ffi_handle = self .ffi_handle - .as_option() + .get() .map(|handle| handle.to_string()) .unwrap_or_else(|| "UNKNOWN".to_owned()); log::info!("MmCtx ({}) has been dropped", ffi_handle) @@ -512,7 +523,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] try_s!(self.close_async_connection().await); - try_s!(self.stop.pin(true)); + try_s!(self.stop.set(true)); // Notify shutdown listeners. self.graceful_shutdown_registry.abort_all().warn_log(); @@ -527,7 +538,7 @@ impl MmArc { #[cfg(not(target_arch = "wasm32"))] async fn close_async_connection(&self) -> Result<(), db_common::async_sql_conn::AsyncConnError> { - if let Some(async_conn) = self.async_sqlite_connection.as_option() { + if let Some(async_conn) = self.async_sqlite_connection.get() { let mut conn = async_conn.lock().await; conn.close().await?; } @@ -560,7 +571,7 @@ impl MmArc { /// Unique context identifier, allowing us to more easily pass the context through the FFI boundaries. pub fn ffi_handle(&self) -> Result { let mut mm_ctx_ffi = try_s!(MM_CTX_FFI.lock()); - if let Some(have) = self.ffi_handle.as_option() { + if let Some(have) = self.ffi_handle.get() { return Ok(*have); } let mut tries = 0; @@ -579,7 +590,7 @@ impl MmArc { Entry::Occupied(_) => continue, // Try another ID. Entry::Vacant(ve) => { ve.insert(self.weak()); - try_s!(self.ffi_handle.pin(rid)); + try_s!(self.ffi_handle.set(rid)); return Ok(rid); }, } @@ -698,25 +709,19 @@ impl SpawnAbortable for MmFutSpawner { /// /// * `ctx_field` - A dedicated crate context field in `MmCtx`, such as the `MmCtx::portfolio_ctx`. /// * `constructor` - Generates the initial crate context. -pub fn from_ctx( - ctx_field: &Mutex>>, - constructor: C, -) -> Result, String> +pub fn from_ctx(ctx: &Mutex>>, init: F) -> Result, String> where - C: FnOnce() -> Result, T: 'static + Send + Sync, + F: FnOnce() -> Result, { - let mut ctx_field = try_s!(ctx_field.lock()); - if let Some(ref ctx) = *ctx_field { - let ctx: Arc = match ctx.clone().downcast() { - Ok(p) => p, - Err(_) => return ERR!("Error casting the context field"), - }; - return Ok(ctx); + let mut guard = try_s!(ctx.lock()); + if let Some(ctx) = guard.as_ref() { + return ctx.clone().downcast().map_err(|_| "Context type mismatch".to_string()); } - let arc = Arc::new(try_s!(constructor())); - *ctx_field = Some(arc.clone()); - Ok(arc) + + let new_ctx = Arc::new(init()?); + *guard = Some(new_ctx.clone()); + Ok(new_ctx) } #[derive(Default)] diff --git a/mm2src/mm2_db/src/indexed_db/drivers/upgrader.rs b/mm2src/mm2_db/src/indexed_db/drivers/upgrader.rs index 8f51a220c1..fa55d87d8c 100644 --- a/mm2src/mm2_db/src/indexed_db/drivers/upgrader.rs +++ b/mm2src/mm2_db/src/indexed_db/drivers/upgrader.rs @@ -29,6 +29,8 @@ pub enum OnUpgradeError { old_version: u32, new_version: u32, }, + #[display(fmt = "Error occurred due to deleting the '{}' index: {}", index, description)] + ErrorDeletingIndex { index: String, description: String }, } pub struct DbUpgrader { @@ -108,4 +110,18 @@ impl TableUpgrader { description: stringify_js_error(&e), }) } + + /// Deletes an index. + /// Regardless of whether the index is created using one or multiple fields, the deleteIndex() + /// method is used to delete any type of index, and it works in the same way for both. + /// https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/deleteIndex + pub fn delete_index(&self, index: &str) -> OnUpgradeResult<()> { + self.object_store + .delete_index(index) + .map(|_| ()) + .map_to_mm(|e| OnUpgradeError::ErrorDeletingIndex { + index: index.to_owned(), + description: stringify_js_error(&e), + }) + } } diff --git a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs index 916854de63..4e2be2acac 100644 --- a/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs +++ b/mm2src/mm2_gui_storage/src/account/storage/sqlite_storage.rs @@ -118,7 +118,7 @@ impl SqliteAccountStorage { pub(crate) fn new(ctx: &MmArc) -> AccountStorageResult { let shared = ctx .sqlite_connection - .as_option() + .get() .or_mm_err(|| AccountStorageError::Internal("'MmCtx::sqlite_connection' is not initialized".to_owned()))?; Ok(SqliteAccountStorage { conn: Arc::clone(shared), diff --git a/mm2src/mm2_main/Cargo.toml b/mm2src/mm2_main/Cargo.toml index 3ddc5e0e09..6019c95694 100644 --- a/mm2src/mm2_main/Cargo.toml +++ b/mm2src/mm2_main/Cargo.toml @@ -22,6 +22,9 @@ default = [] trezor-udp = ["crypto/trezor-udp"] # use for tests to connect to trezor emulator over udp run-device-tests = [] enable-sia = ["coins/enable-sia", "coins_activation/enable-sia"] +sepolia-maker-swap-v2-tests = [] +sepolia-taker-swap-v2-tests = [] +test-ext-api = ["trading_api/test-ext-api"] [dependencies] async-std = { version = "1.5", features = ["unstable"] } @@ -63,16 +66,18 @@ mm2_err_handle = { path = "../mm2_err_handle" } mm2_event_stream = { path = "../mm2_event_stream" } mm2_gui_storage = { path = "../mm2_gui_storage" } mm2_io = { path = "../mm2_io" } -mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p", features = ["application"] } +mm2_libp2p = { path = "../mm2_p2p", package = "mm2_p2p" } mm2_metrics = { path = "../mm2_metrics" } mm2_net = { path = "../mm2_net" } mm2_number = { path = "../mm2_number" } mm2_rpc = { path = "../mm2_rpc", features = ["rpc_facilities"]} mm2_state_machine = { path = "../mm2_state_machine" } +trading_api = { path = "../trading_api" } num-traits = "0.2" parity-util-mem = "0.11" parking_lot = { version = "0.12.0", features = ["nightly"] } primitives = { path = "../mm2_bitcoin/primitives" } +primitive-types = "0.11.1" prost = "0.12" rand = { version = "0.7", features = ["std", "small_rng"] } rand6 = { version = "0.6", package = "rand" } @@ -120,7 +125,9 @@ winapi = "0.3" [dev-dependencies] coins = { path = "../coins", features = ["for-tests", "mocktopus"] } coins_activation = { path = "../coins_activation", features = ["for-tests"] } +common = { path = "../common", features = ["for-tests"] } mm2_test_helpers = { path = "../mm2_test_helpers" } +trading_api = { path = "../trading_api", features = ["mocktopus"] } mocktopus = "0.8.0" testcontainers = "0.15.0" web3 = { git = "https://github.com/KomodoPlatform/rust-web3", tag = "v0.20.0", default-features = false, features = ["http-rustls-tls"] } diff --git a/mm2src/mm2_main/src/ext_api.rs b/mm2src/mm2_main/src/ext_api.rs new file mode 100644 index 0000000000..f1b92c145f --- /dev/null +++ b/mm2src/mm2_main/src/ext_api.rs @@ -0,0 +1,3 @@ +//! RPCs for integration with external third party trading APIs. + +pub mod one_inch; diff --git a/mm2src/mm2_main/src/lp_healthcheck.rs b/mm2src/mm2_main/src/lp_healthcheck.rs index 5e9db51111..20a6004c95 100644 --- a/mm2src/mm2_main/src/lp_healthcheck.rs +++ b/mm2src/mm2_main/src/lp_healthcheck.rs @@ -9,6 +9,7 @@ use instant::{Duration, Instant}; use lazy_static::lazy_static; use mm2_core::mm_ctx::MmArc; use mm2_err_handle::prelude::MmError; +use mm2_err_handle::prelude::*; use mm2_libp2p::p2p_ctx::P2PContext; use mm2_libp2p::{decode_message, encode_message, pub_sub_topic, Libp2pPublic, PeerAddress, TopicPrefix}; use ser_error_derive::SerializeErrorType; @@ -16,7 +17,7 @@ use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::sync::Mutex; -use crate::lp_network::broadcast_p2p_msg; +use crate::lp_network::{broadcast_p2p_msg, P2PRequestError, P2PRequestResult}; pub(crate) const PEER_HEALTHCHECK_PREFIX: TopicPrefix = "hcheck"; @@ -114,7 +115,7 @@ impl HealthcheckMessage { let now_secs = u64::try_from(Utc::now().timestamp()) .map_err(|e| SignValidationError::Internal { reason: e.to_string() })?; - let remaining_expiration_secs = self.data.expires_at_secs - now_secs; + let remaining_expiration_secs = self.data.expires_at_secs.saturating_sub(now_secs); if remaining_expiration_secs == 0 { return Err(SignValidationError::Expired { @@ -279,7 +280,10 @@ pub async fn peer_connection_healthcheck_rpc( Ok(rx.timeout(timeout_duration).await == Ok(Ok(()))) } -pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_libp2p::GossipsubMessage) { +pub(crate) async fn process_p2p_healthcheck_message( + ctx: &MmArc, + message: mm2_libp2p::GossipsubMessage, +) -> P2PRequestResult<()> { macro_rules! try_or_return { ($exp:expr, $msg: expr) => { match $exp { @@ -292,24 +296,17 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } - let data = try_or_return!( - HealthcheckMessage::decode(&message.data), - "Couldn't decode healthcheck message" - ); + let data = HealthcheckMessage::decode(&message.data) + .map_to_mm(|e| P2PRequestError::DecodeError(format!("Couldn't decode healthcheck message: {}", e)))?; + let sender_peer = data.is_received_message_valid().map_to_mm(|e| { + P2PRequestError::ValidationFailed(format!("Received an invalid healthcheck message. Error: {}", e)) + })?; let ctx = ctx.clone(); // Pass the remaining work to another thread to free up this one as soon as possible, // so KDF can handle a high amount of healthcheck messages more efficiently. ctx.spawner().spawn(async move { - let sender_peer = match data.is_received_message_valid() { - Ok(t) => t, - Err(e) => { - log::error!("Received an invalid healthcheck message. Error: {e}"); - return; - }, - }; - if data.should_reply() { // Reply the message so they know we are healthy. @@ -337,6 +334,8 @@ pub(crate) async fn process_p2p_healthcheck_message(ctx: &MmArc, message: mm2_li }; } }); + + Ok(()) } #[cfg(any(test, target_arch = "wasm32"))] diff --git a/mm2src/mm2_main/src/lp_native_dex.rs b/mm2src/mm2_main/src/lp_native_dex.rs index 8e1e91ec13..1e5f7feff0 100644 --- a/mm2src/mm2_main/src/lp_native_dex.rs +++ b/mm2src/mm2_main/src/lp_native_dex.rs @@ -476,7 +476,9 @@ pub async fn lp_init_continue(ctx: MmArc) -> MmInitResult<()> { let balance_update_ordermatch_handler = BalanceUpdateOrdermatchHandler::new(ctx.clone()); register_balance_update_handler(ctx.clone(), Box::new(balance_update_ordermatch_handler)).await; - ctx.initialized.pin(true).map_to_mm(MmInitError::Internal)?; + ctx.initialized + .set(true) + .map_to_mm(|_| MmInitError::Internal("Already Initialized".to_string()))?; // launch kickstart threads before RPC is available, this will prevent the API user to place // an order and start new swap that might get started 2 times because of kick-start diff --git a/mm2src/mm2_main/src/lp_network.rs b/mm2src/mm2_main/src/lp_network.rs index 9de49245f3..f86a37cd29 100644 --- a/mm2src/mm2_main/src/lp_network.rs +++ b/mm2src/mm2_main/src/lp_network.rs @@ -63,6 +63,7 @@ pub enum P2PRequestError { ResponseError(String), #[display(fmt = "Expected 1 response, found {}", _0)] ExpectedSingleResponseError(usize), + ValidationFailed(String), } /// Enum covering error cases that can happen during P2P message processing. @@ -202,15 +203,16 @@ async fn process_p2p_message( to_propagate = true; }, Some(lp_swap::TX_HELPER_PREFIX) => { - if let Some(pair) = split.next() { - if let Ok(Some(coin)) = lp_coinfind(&ctx, pair).await { + if let Some(ticker) = split.next() { + if let Ok(Some(coin)) = lp_coinfind(&ctx, ticker).await { if let Err(e) = coin.tx_enum_from_bytes(&message.data) { log::error!("Message cannot continue the process due to: {:?}", e); return; }; - let fut = coin.send_raw_tx_bytes(&message.data); - ctx.spawner().spawn(async { + if coin.is_utxo_in_native_mode() { + let fut = coin.send_raw_tx_bytes(&message.data); + ctx.spawner().spawn(async { match fut.compat().await { Ok(id) => log::debug!("Transaction broadcasted successfully: {:?} ", id), // TODO (After https://github.com/KomodoPlatform/atomicDEX-API/pull/1433) @@ -219,11 +221,19 @@ async fn process_p2p_message( Err(e) => log::error!("Broadcast transaction failed (ignore this error if the transaction already sent by another seednode). {}", e), }; }) + } } + + to_propagate = true; } }, Some(lp_healthcheck::PEER_HEALTHCHECK_PREFIX) => { - lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await + if let Err(e) = lp_healthcheck::process_p2p_healthcheck_message(&ctx, message).await { + log::error!("{}", e); + return; + } + + to_propagate = true; }, None | Some(_) => (), } @@ -240,9 +250,11 @@ fn process_p2p_request( response_channel: mm2_libp2p::AdexResponseChannel, ) -> P2PRequestResult<()> { let request = decode_message::(&request)?; + log::debug!("Got P2PRequest {:?}", request); + let result = match request { P2PRequest::Ordermatch(req) => lp_ordermatch::process_peer_request(ctx.clone(), req), - P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req), + P2PRequest::NetworkInfo(req) => lp_stats::process_info_request(ctx.clone(), req).map(Some), }; let res = match result { diff --git a/mm2src/mm2_main/src/lp_ordermatch.rs b/mm2src/mm2_main/src/lp_ordermatch.rs index 2f634b5611..1c2ec5ebfe 100644 --- a/mm2src/mm2_main/src/lp_ordermatch.rs +++ b/mm2src/mm2_main/src/lp_ordermatch.rs @@ -638,7 +638,6 @@ impl TryFromBytes for Uuid { } pub fn process_peer_request(ctx: MmArc, request: OrdermatchRequest) -> Result>, String> { - log::debug!("Got ordermatch request {:?}", request); match request { OrdermatchRequest::GetOrderbook { base, rel } => process_get_orderbook_request(ctx, base, rel), OrdermatchRequest::SyncPubkeyOrderbookState { pubkey, trie_roots } => { diff --git a/mm2src/mm2_main/src/lp_stats.rs b/mm2src/mm2_main/src/lp_stats.rs index 185996ecd1..3aedc1cb5c 100644 --- a/mm2src/mm2_main/src/lp_stats.rs +++ b/mm2src/mm2_main/src/lp_stats.rs @@ -11,6 +11,7 @@ use mm2_libp2p::application::request_response::network_info::NetworkInfoRequest; use mm2_libp2p::{encode_message, NetworkInfo, PeerId, RelayAddress, RelayAddressError}; use serde_json::{self as json, Value as Json}; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::sync::Arc; use crate::lp_network::{add_reserved_peer_addresses, lp_network_ports, request_peers, NetIdError, ParseAddressError, @@ -170,16 +171,24 @@ struct Mm2VersionRes { nodes: HashMap, } -fn process_get_version_request(ctx: MmArc) -> Result>, String> { +fn process_get_version_request(ctx: MmArc) -> Result, String> { let response = ctx.mm_version().to_string(); - let encoded = try_s!(encode_message(&response)); - Ok(Some(encoded)) + encode_message(&response).map_err(|e| e.to_string()) } -pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result>, String> { - log::debug!("Got stats request {:?}", request); +fn process_get_peer_utc_timestamp_request() -> Result, String> { + let timestamp = common::get_utc_timestamp(); + let timestamp: u64 = timestamp + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", timestamp)); + + encode_message(×tamp).map_err(|e| e.to_string()) +} + +pub fn process_info_request(ctx: MmArc, request: NetworkInfoRequest) -> Result, String> { match request { NetworkInfoRequest::GetMm2Version => process_get_version_request(ctx), + NetworkInfoRequest::GetPeerUtcTimestamp => process_get_peer_utc_timestamp_request(), } } diff --git a/mm2src/mm2_main/src/lp_swap.rs b/mm2src/mm2_main/src/lp_swap.rs index 48f0d09f2c..9657b225c7 100644 --- a/mm2src/mm2_main/src/lp_swap.rs +++ b/mm2src/mm2_main/src/lp_swap.rs @@ -76,6 +76,7 @@ use derive_more::Display; use http::Response; use mm2_core::mm_ctx::{from_ctx, MmArc}; use mm2_err_handle::prelude::*; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_libp2p::{decode_signed, encode_and_sign, pub_sub_topic, PeerId, TopicPrefix}; use mm2_number::{BigDecimal, MmNumber, MmNumberMultiRepr}; use mm2_state_machine::storable_state_machine::StateMachineStorage; @@ -94,7 +95,7 @@ use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use uuid::Uuid; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::{AtomicU64, Ordering}; mod check_balance; @@ -155,7 +156,9 @@ pub const TX_HELPER_PREFIX: TopicPrefix = "txhlp"; pub(crate) const LEGACY_SWAP_TYPE: u8 = 0; pub(crate) const MAKER_SWAP_V2_TYPE: u8 = 1; pub(crate) const TAKER_SWAP_V2_TYPE: u8 = 2; -const MAX_STARTED_AT_DIFF: u64 = 60; + +pub(crate) const TAKER_FEE_VALIDATION_ATTEMPTS: usize = 6; +pub(crate) const TAKER_FEE_VALIDATION_RETRY_DELAY_SECS: f64 = 10.; const NEGOTIATE_SEND_INTERVAL: f64 = 30.; @@ -171,6 +174,8 @@ pub(crate) const NO_REFUND_FEE: bool = false; /// Sending part of dex fee to the pre-burn account is active pub const PRE_BURN_ACCOUNT_ACTIVE: bool = true; +const MAX_STARTED_AT_DIFF: u64 = MAX_TIME_GAP_FOR_CONNECTED_PEER * 3; + cfg_wasm32! { use mm2_db::indexed_db::{ConstructibleDb, DbLocked}; use saved_swap::migrate_swaps_data; @@ -503,13 +508,13 @@ async fn recv_swap_msg( /// in order to give different and/or heavy communication channels a chance. const BASIC_COMM_TIMEOUT: u64 = 90; -#[cfg(not(feature = "custom-swap-locktime"))] +#[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME const PAYMENT_LOCKTIME: u64 = 3600 * 2 + 300 * 2; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Default atomic swap payment locktime, in seconds. /// Maker sends payment with LOCKTIME * 2 /// Taker sends payment with LOCKTIME @@ -518,9 +523,9 @@ pub(crate) static PAYMENT_LOCKTIME: AtomicU64 = AtomicU64::new(super::CUSTOM_PAY #[inline] /// Returns `PAYMENT_LOCKTIME` pub fn get_payment_locktime() -> u64 { - #[cfg(not(feature = "custom-swap-locktime"))] + #[cfg(not(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests")))] return PAYMENT_LOCKTIME; - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] PAYMENT_LOCKTIME.load(Ordering::Relaxed) } diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap.rs b/mm2src/mm2_main/src/lp_swap/maker_swap.rs index 8610373d46..71ed82b3d0 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap.rs @@ -9,7 +9,8 @@ use super::{broadcast_my_swap_status, broadcast_p2p_tx_msg, broadcast_swap_msg_e LockedAmount, MySwapInfo, NegotiationDataMsg, NegotiationDataV2, NegotiationDataV3, RecoveredSwap, RecoveredSwapAction, SavedSwap, SavedSwapIo, SavedTradeFee, SecretHashAlgo, SwapConfirmationsSettings, SwapError, SwapMsg, SwapPubkeys, SwapTxDataMsg, SwapsContext, TransactionIdentifier, INCLUDE_REFUND_FEE, - NO_REFUND_FEE, WAIT_CONFIRM_INTERVAL_SEC}; + NO_REFUND_FEE, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, + WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_dispatcher::{DispatcherContext, LpEvents}; use crate::lp_network::subscribe_to_topic; use crate::lp_ordermatch::MakerOrderBuilder; @@ -26,7 +27,7 @@ use coins::{dex_fee_from_taker_coin, CanRefundHtlc, CheckIfMyPaymentSentArgs, Co FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, SwapTxTypeWithSecretHash, TradeFee, TradePreimageValue, TransactionEnum, ValidateFeeArgs, - ValidatePaymentInput, MIN_SWAP_PROTOCOL_VERSION}; + ValidatePaymentInput, WatcherReward, MIN_SWAP_PROTOCOL_VERSION}; use common::log::{debug, error, info, warn}; use common::{bits256, executor::Timer, now_ms}; use common::{env_var_as_bool, now_sec, wait_until_sec}; @@ -857,13 +858,13 @@ impl MakerSwap { { Ok(_) => break, Err(err) => { - if attempts >= 6 { + if attempts >= TAKER_FEE_VALIDATION_ATTEMPTS { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::TakerFeeValidateFailed(ERRL!("{}", err).into()), ])); } else { attempts += 1; - Timer::sleep(10.).await; + Timer::sleep(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS).await; } }, }; @@ -878,83 +879,54 @@ impl MakerSwap { Ok((Some(MakerSwapCommand::SendPayment), swap_events)) } - async fn maker_payment(&self) -> Result<(Option, Vec), String> { - let lock_duration = self.r().data.lock_duration; - let timeout = self.r().data.started_at + lock_duration / 3; - let now = now_sec(); - if now > timeout { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); + /// Sets up the watcher reward for the maker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, wait_maker_payment_until: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); } + self.maker_coin + .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) + .await + .map_err(|err| err.into_inner().to_string()) + } + + async fn maker_payment(&self) -> Result<(Option, Vec), String> { + // Extract values from lock before async operations + let lock_duration = self.r().data.lock_duration; let maker_payment_lock = self.r().data.maker_payment_lock; let other_maker_coin_htlc_pub = self.r().other_maker_coin_htlc_pub; let secret_hash = self.secret_hash(); let maker_coin_swap_contract_address = self.r().data.maker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let payment_instructions = self.r().payment_instructions.clone(); - let transaction_f = self.maker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: maker_payment_lock, - other_pub: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - search_from_block: self.r().data.maker_coin_start_block, - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &self.maker_amount, - payment_instructions: &payment_instructions, - }); - + let maker_coin_start_block = self.r().data.maker_coin_start_block; let wait_maker_payment_until = wait_for_maker_payment_conf_until(self.r().data.started_at, lock_duration); - let watcher_reward = if self.r().watcher_reward { - match self - .maker_coin - .get_maker_watcher_reward(&self.taker_coin, self.watcher_reward_amount(), wait_maker_payment_until) - .await - { - Ok(reward) => reward, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed(err.into_inner().to_string().into()), - ])) - }, - } - } else { - None - }; - let transaction = match transaction_f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let payment = self - .maker_coin - .send_maker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock: maker_payment_lock, - other_pubkey: &*other_maker_coin_htlc_pub, - secret_hash: secret_hash.as_slice(), - amount: self.maker_amount.clone(), - swap_contract_address: &maker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: wait_maker_payment_until, - }) - .await; - - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(MakerSwapCommand::Finish), vec![ - MakerSwapEvent::MakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent maker payment in case of restart + let maybe_existing_payment = match self + .maker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: maker_payment_lock, + other_pub: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + search_from_block: maker_coin_start_block, + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &self.maker_amount, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(MakerSwapCommand::Finish), vec![ MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -962,6 +934,60 @@ impl MakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.started_at + lock_duration / 3; + let now = now_sec(); + if now > timeout { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enabled + let watcher_reward = match self.setup_watcher_reward(wait_maker_payment_until).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed(err.into()), + ])) + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + match self + .maker_coin + .send_maker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: maker_payment_lock, + other_pubkey: &*other_maker_coin_htlc_pub, + secret_hash: secret_hash.as_slice(), + amount: self.maker_amount.clone(), + swap_contract_address: &maker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: wait_maker_payment_until, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(MakerSwapCommand::Finish), vec![ + MakerSwapEvent::MakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])); + }, + } + }, + }; + + // Build transaction identifier and prepare events let tx_hash = transaction.tx_hash_as_bytes(); info!("{}: Maker payment tx {:02x}", MAKER_PAYMENT_SENT_LOG, tx_hash); diff --git a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs index 2751a32f80..621d82daf3 100644 --- a/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/maker_swap_v2.rs @@ -1478,6 +1478,7 @@ impl MmResult<(), SavedSwapError> { diff --git a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs index 6488e466de..982ea575e7 100644 --- a/mm2src/mm2_main/src/lp_swap/swap_watcher.rs +++ b/mm2src/mm2_main/src/lp_swap/swap_watcher.rs @@ -1,5 +1,6 @@ use super::{broadcast_p2p_tx_msg, get_payment_locktime, lp_coinfind, taker_payment_spend_deadline, tx_helper_topic, - H256Json, SwapsContext, WAIT_CONFIRM_INTERVAL_SEC}; + H256Json, SwapsContext, TAKER_FEE_VALIDATION_ATTEMPTS, TAKER_FEE_VALIDATION_RETRY_DELAY_SECS, + WAIT_CONFIRM_INTERVAL_SEC}; use crate::lp_network::{P2PRequestError, P2PRequestResult}; use crate::MmError; @@ -181,23 +182,30 @@ impl State for ValidateTakerFee { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher validate taker fee"); - let validated_f = watcher_ctx - .taker_coin - .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { - taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), - sender_pubkey: watcher_ctx.verified_pub.clone(), - min_block_number: watcher_ctx.data.taker_coin_start_block, - lock_duration: watcher_ctx.data.lock_duration, - }) - .compat(); - if let Err(err) = validated_f.await { - return Self::change_state(Stopped::from_reason(StopReason::Error( - WatcherError::InvalidTakerFee(format!("{:?}", err)).into(), - ))); - }; - - Self::change_state(ValidateTakerPayment {}) + let validation_result = retry_on_err!(async { + watcher_ctx + .taker_coin + .watcher_validate_taker_fee(WatcherValidateTakerFeeInput { + taker_fee_hash: watcher_ctx.data.taker_fee_hash.clone(), + sender_pubkey: watcher_ctx.verified_pub.clone(), + min_block_number: watcher_ctx.data.taker_coin_start_block, + lock_duration: watcher_ctx.data.lock_duration, + }) + .compat() + .await + }) + .repeat_every_secs(TAKER_FEE_VALIDATION_RETRY_DELAY_SECS) + .attempts(TAKER_FEE_VALIDATION_ATTEMPTS) + .inspect_err(|e| error!("Error validating taker fee: {}", e)) + .await; + + match validation_result { + Ok(_) => Self::change_state(ValidateTakerPayment {}), + Err(repeat_err) => Self::change_state(Stopped::from_reason(StopReason::Error( + WatcherError::InvalidTakerFee(repeat_err.to_string()).into(), + ))), + } } } @@ -249,10 +257,7 @@ impl State for ValidateTakerPayment { let validate_input = WatcherValidatePaymentInput { payment_tx: taker_payment_hex.clone(), taker_payment_refund_preimage: watcher_ctx.data.taker_payment_refund_preimage.clone(), - time_lock: match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => watcher_ctx.data.swap_started_at, - Err(_) => watcher_ctx.taker_locktime(), - }, + time_lock: watcher_ctx.taker_locktime(), taker_pub: watcher_ctx.verified_pub.clone(), maker_pub: watcher_ctx.data.maker_pub.clone(), secret_hash: watcher_ctx.data.secret_hash.clone(), @@ -345,17 +350,20 @@ impl State for WaitForTakerPaymentSpend { }, }; - let f = watcher_ctx.maker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { - tx_bytes: &maker_payment_hex, - secret_hash: &watcher_ctx.data.secret_hash, - wait_until, - from_block: watcher_ctx.data.maker_coin_start_block, - swap_contract_address: &None, - check_every: payment_search_interval, - watcher_reward: watcher_ctx.watcher_reward, - }); - - if f.compat().await.is_ok() { + if watcher_ctx + .maker_coin + .wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + tx_bytes: &maker_payment_hex, + secret_hash: &watcher_ctx.data.secret_hash, + wait_until, + from_block: watcher_ctx.data.maker_coin_start_block, + swap_contract_address: &None, + check_every: payment_search_interval, + watcher_reward: watcher_ctx.watcher_reward, + }) + .await + .is_ok() + { info!("{}", MAKER_PAYMENT_SPEND_FOUND_LOG); return Self::change_state(Stopped::from_reason(StopReason::Finished( WatcherSuccess::MakerPaymentSpentByTaker, @@ -439,20 +447,18 @@ impl State for RefundTakerPayment { async fn on_changed(self: Box, watcher_ctx: &mut WatcherStateMachine) -> StateResult { debug!("Watcher refund taker payment"); - if std::env::var("USE_TEST_LOCKTIME").is_err() { - loop { - match watcher_ctx - .taker_coin - .can_refund_htlc(watcher_ctx.taker_locktime()) - .await - { - Ok(CanRefundHtlc::CanRefundNow) => break, - Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, - Err(e) => { - error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); - Timer::sleep(30.).await; - }, - } + loop { + match watcher_ctx + .taker_coin + .can_refund_htlc(watcher_ctx.taker_locktime()) + .await + { + Ok(CanRefundHtlc::CanRefundNow) => break, + Ok(CanRefundHtlc::HaveToWait(to_sleep)) => Timer::sleep(to_sleep as f64).await, + Err(e) => { + error!("Error {} on can_refund_htlc, retrying in 30 seconds", e); + Timer::sleep(30.).await; + }, } } diff --git a/mm2src/mm2_main/src/lp_swap/taker_restart.rs b/mm2src/mm2_main/src/lp_swap/taker_restart.rs index d934b6b11e..014f671bd1 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_restart.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_restart.rs @@ -154,11 +154,7 @@ pub async fn check_taker_payment_spend(swap: &TakerSwap) -> Result swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let unique_data = swap.unique_swap_data(); let watcher_reward = swap.r().watcher_reward; @@ -223,10 +219,7 @@ pub async fn add_taker_payment_refunded_by_watcher_event( ) -> Result { let other_maker_coin_htlc_pub = swap.r().other_maker_coin_htlc_pub; let taker_coin_swap_contract_address = swap.r().data.taker_coin_swap_contract_address.clone(); - let taker_payment_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => swap.r().data.started_at, - Err(_) => swap.r().data.taker_payment_lock, - }; + let taker_payment_lock = swap.r().data.taker_payment_lock; let secret_hash = swap.r().secret_hash.0.clone(); let validate_input = ValidateWatcherSpendInput { diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap.rs b/mm2src/mm2_main/src/lp_swap/taker_swap.rs index 45eae4b02d..0e4cc81046 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap.rs @@ -25,8 +25,8 @@ use coins::TEST_BURN_ADDR_RAW_PUBKEY; use coins::{dex_fee_from_taker_coin, lp_coinfind, CanRefundHtlc, CheckIfMyPaymentSentArgs, ConfirmPaymentInput, DexFee, FeeApproxStage, FoundSwapTxSpend, MmCoin, MmCoinEnum, PaymentInstructionArgs, PaymentInstructions, PaymentInstructionsErr, RefundPaymentArgs, SearchForSwapTxSpendInput, SendPaymentArgs, SpendPaymentArgs, - SwapTxTypeWithSecretHash, TradeFee, TradePreimageValue, ValidatePaymentInput, WaitForHTLCTxSpendArgs, - MIN_SWAP_PROTOCOL_VERSION}; + SwapTxTypeWithSecretHash, TradeFee, TradePreimageValue, TransactionEnum, ValidatePaymentInput, + WaitForHTLCTxSpendArgs, WatcherReward, MIN_SWAP_PROTOCOL_VERSION}; use common::executor::Timer; use common::log::{debug, error, info, warn}; use common::{bits256, env_var_as_bool, now_ms, now_sec, wait_until_sec}; @@ -1597,6 +1597,104 @@ impl TakerSwap { } } + /// Sets up the watcher reward for the taker's payment in the swap. + /// + /// The reward mainly serves as compensation to watchers for the mining fees + /// paid to execute the transactions. + /// + /// The reward configuration depends on the specific requirements of the coins + /// involved in the swap. + /// Some coins may not support watcher rewards at all. + async fn setup_watcher_reward(&self, taker_payment_lock: u64) -> Result, String> { + if !self.r().watcher_reward { + return Ok(None); + } + + let reward_amount = self.r().reward_amount.clone(); + self.taker_coin + .get_taker_watcher_reward( + &self.maker_coin, + Some(self.taker_amount.clone().into()), + Some(self.maker_amount.clone().into()), + reward_amount, + taker_payment_lock, + ) + .await + .map(Some) + .map_err(|err| ERRL!("Watcher reward error: {}", err.to_string())) + } + + /// Processes watcher-related logic for the swap by preparing and broadcasting necessary data. + /// + /// This function creates spend/refund preimages and broadcasts them to watchers if both coins + /// support watcher functionality and watchers are enabled. + /// + /// The preimages allow watchers to either complete the swap by spending the maker payment + /// or refund the taker payment if needed. + async fn process_watcher_logic(&self, transaction: &TransactionEnum) -> Option { + let watchers_enabled_and_supported = self.ctx.use_watchers() + && self.taker_coin.is_supported_by_watchers() + && self.maker_coin.is_supported_by_watchers(); + + if !watchers_enabled_and_supported { + return None; + } + + let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( + &self.r().maker_payment.as_ref().unwrap().tx_hex, + self.maker_payment_lock.load(Ordering::Relaxed), + self.r().other_maker_coin_htlc_pub.as_slice(), + &self.r().secret_hash.0, + &self.unique_swap_data()[..], + ); + + let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( + &transaction.tx_hex(), + self.r().data.taker_payment_lock, + &*self.r().other_taker_coin_htlc_pub, + &self.r().secret_hash.0, + &self.r().data.taker_coin_swap_contract_address, + &self.unique_swap_data(), + ); + + match try_join( + maker_payment_spend_preimage_fut.compat(), + taker_payment_refund_preimage_fut.compat(), + ) + .await + { + Ok((maker_payment_spend, taker_payment_refund)) => { + let watcher_data = self.create_watcher_data( + transaction.tx_hash_as_bytes().into_vec(), + maker_payment_spend.tx_hex(), + taker_payment_refund.tx_hex(), + ); + let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); + + let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); + broadcast_swap_message( + &self.ctx, + watcher_topic(&self.r().data.taker_coin), + swpmsg_watcher, + &Some(htlc_keypair), + ); + + info!("{}", WATCHER_MESSAGE_SENT_LOG); + Some(TakerSwapEvent::WatcherMessageSent( + Some(maker_payment_spend.tx_hex()), + Some(taker_payment_refund.tx_hex()), + )) + }, + Err(e) => { + error!( + "The watcher message could not be sent, error creating at least one of the preimages: {}", + e.get_plain_text_format() + ); + None + }, + } + } + async fn send_taker_payment(&self) -> Result<(Option, Vec), String> { #[cfg(test)] if self.fail_at == Some(FailAt::TakerPayment) { @@ -1605,96 +1703,33 @@ impl TakerSwap { ])); } - let timeout = self.r().data.maker_payment_wait; - let now = now_sec(); - if now > timeout { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), - ])); - } - + // Extract values from the lock before async operations let taker_payment_lock = self.r().data.taker_payment_lock; let other_taker_coin_htlc_pub = self.r().other_taker_coin_htlc_pub; let secret_hash = self.r().secret_hash.clone(); + let taker_coin_start_block = self.r().data.taker_coin_start_block; let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); let unique_data = self.unique_swap_data(); let taker_amount_decimal = self.taker_amount.to_decimal(); let payment_instructions = self.r().payment_instructions.clone(); - let f = self.taker_coin.check_if_my_payment_sent(CheckIfMyPaymentSentArgs { - time_lock: taker_payment_lock, - other_pub: other_taker_coin_htlc_pub.as_slice(), - secret_hash: &secret_hash.0, - search_from_block: self.r().data.taker_coin_start_block, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - amount: &taker_amount_decimal, - payment_instructions: &payment_instructions, - }); - - let reward_amount = self.r().reward_amount.clone(); - let wait_until = taker_payment_lock; - let watcher_reward = if self.r().watcher_reward { - match self - .taker_coin - .get_taker_watcher_reward( - &self.maker_coin, - Some(self.taker_amount.clone().into()), - Some(self.maker_amount.clone().into()), - reward_amount, - wait_until, - ) - .await - { - Ok(reward) => Some(reward), - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("Watcher reward error: {}", err.to_string()).into(), - ), - ])) - }, - } - } else { - None - }; - let transaction = match f.await { - Ok(res) => match res { - Some(tx) => tx, - None => { - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => taker_payment_lock, - }; - let lock_duration = self.r().data.lock_duration; - let payment = self - .taker_coin - .send_taker_payment(SendPaymentArgs { - time_lock_duration: lock_duration, - time_lock, - other_pubkey: &*other_taker_coin_htlc_pub, - secret_hash: &secret_hash.0, - amount: taker_amount_decimal, - swap_contract_address: &taker_coin_swap_contract_address, - swap_unique_data: &unique_data, - payment_instructions: &payment_instructions, - watcher_reward, - wait_for_confirmation_until: taker_payment_lock, - }) - .await; - - match payment { - Ok(t) => t, - Err(err) => { - return Ok((Some(TakerSwapCommand::Finish), vec![ - TakerSwapEvent::TakerPaymentTransactionFailed( - ERRL!("{}", err.get_plain_text_format()).into(), - ), - ])); - }, - } - }, - }, + // Look for previously sent taker payment in case of restart + let maybe_existing_payment = match self + .taker_coin + .check_if_my_payment_sent(CheckIfMyPaymentSentArgs { + time_lock: taker_payment_lock, + other_pub: other_taker_coin_htlc_pub.as_slice(), + secret_hash: &secret_hash.0, + search_from_block: taker_coin_start_block, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + amount: &taker_amount_decimal, + payment_instructions: &payment_instructions, + }) + .await + { + Ok(Some(tx)) => Some(tx), + Ok(None) => None, Err(e) => { return Ok((Some(TakerSwapCommand::Finish), vec![ TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("{}", e).into()), @@ -1702,6 +1737,61 @@ impl TakerSwap { }, }; + // If the payment is not yet sent, make sure we didn't miss the deadline for sending it. + if maybe_existing_payment.is_none() { + let timeout = self.r().data.maker_payment_wait; + let now = now_sec(); + if now > timeout { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(ERRL!("Timeout {} > {}", now, timeout).into()), + ])); + } + } + + // Set up watcher reward if enable + let watcher_reward = match self.setup_watcher_reward(taker_payment_lock).await { + Ok(reward) => reward, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed(err.into()), + ])); + }, + }; + + // Use existing payment or create new one + let transaction = match maybe_existing_payment { + Some(tx) => tx, + None => { + let lock_duration = self.r().data.lock_duration; + match self + .taker_coin + .send_taker_payment(SendPaymentArgs { + time_lock_duration: lock_duration, + time_lock: taker_payment_lock, + other_pubkey: &*other_taker_coin_htlc_pub, + secret_hash: &secret_hash.0, + amount: taker_amount_decimal, + swap_contract_address: &taker_coin_swap_contract_address, + swap_unique_data: &unique_data, + payment_instructions: &payment_instructions, + watcher_reward, + wait_for_confirmation_until: taker_payment_lock, + }) + .await + { + Ok(t) => t, + Err(err) => { + return Ok((Some(TakerSwapCommand::Finish), vec![ + TakerSwapEvent::TakerPaymentTransactionFailed( + ERRL!("{}", err.get_plain_text_format()).into(), + ), + ])) + }, + } + }, + }; + + // Create transaction identifier and prepare `TakerPaymentSent` success event let tx_hash = transaction.tx_hash_as_bytes(); let tx_hex = BytesJson::from(transaction.tx_hex()); info!("Taker payment tx hash {:02x}", tx_hash); @@ -1709,65 +1799,11 @@ impl TakerSwap { tx_hex: tx_hex.clone(), tx_hash, }; - let mut swap_events = vec![TakerSwapEvent::TakerPaymentSent(tx_ident)]; - if self.ctx.use_watchers() - && self.taker_coin.is_supported_by_watchers() - && self.maker_coin.is_supported_by_watchers() - { - let maker_payment_spend_preimage_fut = self.maker_coin.create_maker_payment_spend_preimage( - &self.r().maker_payment.as_ref().unwrap().tx_hex, - self.maker_payment_lock.load(Ordering::Relaxed), - self.r().other_maker_coin_htlc_pub.as_slice(), - &self.r().secret_hash.0, - &self.unique_swap_data()[..], - ); - - let time_lock = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - let taker_payment_refund_preimage_fut = self.taker_coin.create_taker_payment_refund_preimage( - &transaction.tx_hex(), - time_lock, - &*self.r().other_taker_coin_htlc_pub, - &self.r().secret_hash.0, - &self.r().data.taker_coin_swap_contract_address, - &self.unique_swap_data(), - ); - let payment_fut_pair = try_join( - maker_payment_spend_preimage_fut.compat(), - taker_payment_refund_preimage_fut.compat(), - ); - - match payment_fut_pair.await { - Ok((maker_payment_spend, taker_payment_refund)) => { - let watcher_data = self.create_watcher_data( - transaction.tx_hash_as_bytes().into_vec(), - maker_payment_spend.tx_hex(), - taker_payment_refund.tx_hex(), - ); - let swpmsg_watcher = SwapWatcherMsg::TakerSwapWatcherMsg(watcher_data); - - let htlc_keypair = self.taker_coin.derive_htlc_key_pair(&self.unique_swap_data()); - broadcast_swap_message( - &self.ctx, - watcher_topic(&self.r().data.taker_coin), - swpmsg_watcher, - &Some(htlc_keypair), - ); - swap_events.push(TakerSwapEvent::WatcherMessageSent( - Some(maker_payment_spend.tx_hex()), - Some(taker_payment_refund.tx_hex()), - )); - info!("{}", WATCHER_MESSAGE_SENT_LOG); - }, - Err(e) => error!( - "The watcher message could not be sent, error creating at least one of the preimages: {}", - e.get_plain_text_format() - ), - } + // Process watcher logic if enabled and supported by both coins + if let Some(watcher_event) = self.process_watcher_logic(&transaction).await { + swap_events.push(watcher_event); } Ok((Some(TakerSwapCommand::WaitForTakerPaymentSpend), swap_events)) @@ -1776,7 +1812,7 @@ impl TakerSwap { async fn wait_for_taker_payment_spend(&self) -> Result<(Option, Vec), String> { const BROADCAST_MSG_INTERVAL_SEC: f64 = 600.; - let tx_hex = self.r().taker_payment.as_ref().unwrap().tx_hex.0.clone(); + let tx_hex = self.r().taker_payment.as_ref().unwrap().tx_hex.clone(); let mut watcher_broadcast_abort_handle = None; // Watchers cannot be used for lightning swaps for now // Todo: Check if watchers can work in some cases with lightning and implement it if it's possible, this part will probably work if only the taker is lightning since the preimage is available @@ -1806,7 +1842,7 @@ impl TakerSwap { } // Todo: taker_payment should be a message on lightning network not a swap message - let msg = SwapMsg::TakerPayment(tx_hex); + let msg = SwapMsg::TakerPayment(tx_hex.0.clone()); let send_abort_handle = broadcast_swap_msg_every( self.ctx.clone(), vec![(swap_topic(&self.uuid), msg)], @@ -1823,21 +1859,21 @@ impl TakerSwap { info!("Waiting for maker to spend taker payment!"); - let wait_until = match std::env::var("USE_TEST_LOCKTIME") { - Ok(_) => self.r().data.started_at, - Err(_) => self.r().data.taker_payment_lock, - }; - + let wait_until = self.r().data.taker_payment_lock; + let secret_hash = self.r().secret_hash.clone(); + let taker_coin_start_block = self.r().data.taker_coin_start_block; + let taker_coin_swap_contract_address = self.r().data.taker_coin_swap_contract_address.clone(); + let watcher_reward = self.r().watcher_reward; let f = self.taker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { - tx_bytes: &self.r().taker_payment.clone().unwrap().tx_hex, - secret_hash: &self.r().secret_hash.0, + tx_bytes: &tx_hex, + secret_hash: &secret_hash.0, wait_until, - from_block: self.r().data.taker_coin_start_block, - swap_contract_address: &self.r().data.taker_coin_swap_contract_address, + from_block: taker_coin_start_block, + swap_contract_address: &taker_coin_swap_contract_address, check_every: TAKER_PAYMENT_SPEND_SEARCH_INTERVAL, - watcher_reward: self.r().watcher_reward, + watcher_reward, }); - let tx = match f.compat().await { + let tx = match f.await { Ok(t) => t, Err(err) => { return Ok((Some(TakerSwapCommand::PrepareForTakerPaymentRefund), vec![ @@ -1857,8 +1893,6 @@ impl TakerSwap { tx_hash, }; - let secret_hash = self.r().secret_hash.clone(); - let watcher_reward = self.r().watcher_reward; let secret = match self .taker_coin .extract_secret(&secret_hash.0, &tx_ident.tx_hex, watcher_reward) diff --git a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs index 112eb00b73..00b27946e6 100644 --- a/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs +++ b/mm2src/mm2_main/src/lp_swap/taker_swap_v2.rs @@ -2112,6 +2112,7 @@ impl tx, diff --git a/mm2src/mm2_main/src/lp_wallet.rs b/mm2src/mm2_main/src/lp_wallet.rs index 559821a26a..abe5663024 100644 --- a/mm2src/mm2_main/src/lp_wallet.rs +++ b/mm2src/mm2_main/src/lp_wallet.rs @@ -139,7 +139,7 @@ async fn read_and_decrypt_passphrase_if_available( Some(encrypted_passphrase) => { let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password) .mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?; - Ok(Some(mnemonic.to_string())) + Ok(Some(mnemonic)) }, None => Ok(None), } @@ -214,7 +214,7 @@ async fn decrypt_validate_or_save_passphrase( wallet_password: &str, ) -> WalletInitResult> { // Decrypt the provided encrypted passphrase - let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?.to_string(); + let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?; match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? { Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => { @@ -305,8 +305,8 @@ fn initialize_crypto_context(ctx: &MmArc, passphrase: &str) -> WalletInitResult< pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResult<()> { let (wallet_name, passphrase) = deserialize_wallet_config(ctx)?; ctx.wallet_name - .pin(wallet_name.clone()) - .map_to_mm(WalletInitError::InternalError)?; + .set(wallet_name.clone()) + .map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?; let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?; if let Some(passphrase) = passphrase { @@ -541,7 +541,7 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult>` to handle the case where the wallet name is not set. // `wallet_name` can be `None` in the case of no-login mode. - let activated_wallet = ctx.wallet_name.ok_or(GetWalletsError::Internal( + let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal( "`wallet_name` not initialized yet!".to_string(), ))?; diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs index c873b2d5ff..e779f7b86a 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs @@ -47,6 +47,7 @@ pub(super) async fn save_encrypted_passphrase( pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult> { let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsStorageError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? diff --git a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs index a815bfcca1..fa66cada1c 100644 --- a/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs +++ b/mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs @@ -126,6 +126,7 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle let wallet_name = ctx .wallet_name + .get() .ok_or(WalletsDBError::Internal( "`wallet_name` not initialized yet!".to_string(), ))? diff --git a/mm2src/mm2_main/src/mm2.rs b/mm2src/mm2_main/src/mm2.rs index dd7c7bed27..151b6de1eb 100644 --- a/mm2src/mm2_main/src/mm2.rs +++ b/mm2src/mm2_main/src/mm2.rs @@ -47,10 +47,11 @@ use common::log::LogLevel; use common::password_policy::password_policy; use mm2_core::mm_ctx::MmCtxBuilder; -#[cfg(feature = "custom-swap-locktime")] use common::log::warn; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] +use common::log::warn; +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use lp_swap::PAYMENT_LOCKTIME; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] use std::sync::atomic::Ordering; use gstuff::slurp; @@ -85,7 +86,7 @@ pub mod rpc; pub const PASSWORD_MAXIMUM_CONSECUTIVE_CHARACTERS: usize = 3; -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] const CUSTOM_PAYMENT_LOCKTIME_DEFAULT: u64 = 900; pub struct LpMainParams { @@ -102,7 +103,7 @@ impl LpMainParams { } } -#[cfg(feature = "custom-swap-locktime")] +#[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] /// Reads `payment_locktime` from conf arg and assigns it into `PAYMENT_LOCKTIME` in lp_swap. /// Assigns 900 if `payment_locktime` is invalid or not provided. fn initialize_payment_locktime(conf: &Json) { @@ -150,7 +151,7 @@ pub async fn lp_main( } } - #[cfg(feature = "custom-swap-locktime")] + #[cfg(any(feature = "custom-swap-locktime", test, feature = "run-docker-tests"))] initialize_payment_locktime(&conf); let ctx = MmCtxBuilder::new() @@ -277,7 +278,7 @@ pub fn mm2_main(version: String, datetime: String) { } if first_arg == Some("--version") || first_arg == Some("-v") || first_arg == Some("version") { - println!("AtomicDEX API: {version}"); + println!("Komodo DeFi Framework: {version}"); return; } @@ -291,7 +292,7 @@ pub fn mm2_main(version: String, datetime: String) { return; } - log!("AtomicDEX API {} DT {}", version, datetime); + log!("Komodo DeFi Framework {} DT {}", version, datetime); if let Err(err) = run_lp_main(first_arg, &|_| (), version, datetime) { log!("{}", err); @@ -303,22 +304,23 @@ pub fn mm2_main(version: String, datetime: String) { /// Parses and returns the `first_arg` as JSON. /// Attempts to load the config from `MM2.json` file if `first_arg` is None pub fn get_mm2config(first_arg: Option<&str>) -> Result { - let conf_path = common::kdf_config_file(); - let conf_from_file = slurp(&conf_path); let conf = match first_arg { - Some(s) => s, + Some(s) => s.to_owned(), None => { + let conf_path = common::kdf_config_file().map_err(|e| e.to_string())?; + let conf_from_file = slurp(&conf_path); + if conf_from_file.is_empty() { return ERR!( "Config is not set from command line arg and {} file doesn't exist.", conf_path.display() ); } - try_s!(std::str::from_utf8(&conf_from_file)) + try_s!(String::from_utf8(conf_from_file)) }, }; - let mut conf: Json = match json::from_str(conf) { + let mut conf: Json = match json::from_str(&conf) { Ok(json) => json, // Syntax or io errors may include the conf string in the error message so we don't want to take risks and show these errors internals in the log. // If new variants are added to the Error enum, there can be a risk of exposing the conf string in the error message when updating serde_json so @@ -327,7 +329,7 @@ pub fn get_mm2config(first_arg: Option<&str>) -> Result { }; if conf["coins"].is_null() { - let coins_path = common::kdf_coins_file(); + let coins_path = common::kdf_coins_file().map_err(|e| e.to_string())?; let coins_from_file = slurp(&coins_path); if coins_from_file.is_empty() { diff --git a/mm2src/mm2_main/src/ordermatch_tests.rs b/mm2src/mm2_main/src/ordermatch_tests.rs index 1ac83697af..3bf81d6370 100644 --- a/mm2src/mm2_main/src/ordermatch_tests.rs +++ b/mm2src/mm2_main/src/ordermatch_tests.rs @@ -1055,7 +1055,10 @@ fn test_cancel_by_single_coin() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1074,7 +1077,10 @@ fn test_cancel_by_pair() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); @@ -1097,7 +1103,10 @@ fn test_cancel_by_all() { let rx = prepare_for_cancel_by(&ctx); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); delete_my_maker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); delete_my_taker_order.mock_safe(|_, _, _| MockResult::Return(Box::new(futures01::future::ok(())))); diff --git a/mm2src/mm2_main/src/rpc.rs b/mm2src/mm2_main/src/rpc.rs index 85b61db612..4e4947e151 100644 --- a/mm2src/mm2_main/src/rpc.rs +++ b/mm2src/mm2_main/src/rpc.rs @@ -22,10 +22,10 @@ use crate::rpc::rate_limiter::RateLimitError; use common::log::{error, info}; -use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode, APPLICATION_JSON}; +use common::{err_to_rpc_json_string, err_tp_rpc_json, HttpStatusCode}; use derive_more::Display; use futures::future::{join_all, FutureExt}; -use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE}; +use http::header::{HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN}; use http::request::Parts; use http::{Method, Request, Response, StatusCode}; use mm2_core::mm_ctx::MmArc; @@ -44,9 +44,7 @@ cfg_native! { #[path = "rpc/dispatcher/dispatcher.rs"] mod dispatcher; #[path = "rpc/dispatcher/dispatcher_legacy.rs"] mod dispatcher_legacy; -#[path = "rpc/lp_commands/lp_commands.rs"] pub mod lp_commands; -#[path = "rpc/lp_commands/lp_commands_legacy.rs"] -pub mod lp_commands_legacy; +pub mod lp_commands; mod rate_limiter; /// Lists the RPC method not requiring the "userpass" authentication. @@ -205,8 +203,6 @@ async fn process_single_request(ctx: MmArc, req: Json, client: SocketAddr) -> Re #[cfg(not(target_arch = "wasm32"))] async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Response { - const NON_ALLOWED_CHARS: &[char] = &['<', '>', '&']; - /// Unwraps a result or propagates its error 500 response with the specified headers (if they are present). macro_rules! try_sf { ($value: expr $(, $header_key:expr => $header_val:expr)*) => { @@ -265,19 +261,6 @@ async fn rpc_service(req: Request, ctx_h: u32, client: SocketAddr) -> Resp let req_json = { let req_bytes = try_sf!(hyper::body::to_bytes(req_body).await, ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors); - let req_str = String::from_utf8_lossy(req_bytes.as_ref()); - let is_invalid_input = req_str.chars().any(|c| NON_ALLOWED_CHARS.contains(&c)); - if is_invalid_input { - return Response::builder() - .status(500) - .header(ACCESS_CONTROL_ALLOW_ORIGIN, rpc_cors) - .header(CONTENT_TYPE, APPLICATION_JSON) - .body(Body::from(err_to_rpc_json_string(&format!( - "Invalid input: contains one or more of the following non-allowed characters: {:?}", - NON_ALLOWED_CHARS - )))) - .unwrap(); - } try_sf!(json::from_slice(&req_bytes), ACCESS_CONTROL_ALLOW_ORIGIN => rpc_cors) }; @@ -427,7 +410,7 @@ pub extern "C" fn spawn_rpc(ctx_h: u32) { $port, now_sec() ); - let _ = $ctx.rpc_started.pin(true); + let _ = $ctx.rpc_started.set(true); server }); } @@ -496,7 +479,7 @@ pub fn spawn_rpc(ctx_h: u32) { use std::sync::Mutex; let ctx = MmArc::from_ffi_handle(ctx_h).expect("No context"); - if ctx.wasm_rpc.is_some() { + if ctx.wasm_rpc.get().is_some() { error!("RPC is initialized already"); return; } @@ -529,12 +512,12 @@ pub fn spawn_rpc(ctx_h: u32) { ctx.spawner().spawn(fut); // even if the [`MmCtx::wasm_rpc`] is initialized already, the spawned future above will be shutdown - if let Err(e) = ctx.wasm_rpc.pin(request_tx) { - error!("'MmCtx::wasm_rpc' is initialized already: {}", e); + if ctx.wasm_rpc.set(request_tx).is_err() { + error!("'MmCtx::wasm_rpc' is initialized already"); return; }; - if let Err(e) = ctx.rpc_started.pin(true) { - error!("'MmCtx::rpc_started' is set already: {}", e); + if ctx.rpc_started.set(true).is_err() { + error!("'MmCtx::rpc_started' is set already"); return; } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs index ba99379892..fd9babd2c5 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs @@ -5,15 +5,25 @@ use crate::lp_native_dex::init_hw::{cancel_init_trezor, init_trezor, init_trezor use crate::lp_native_dex::init_metamask::{cancel_connect_metamask, connect_metamask, connect_metamask_status}; use crate::lp_ordermatch::{best_orders_rpc_v2, orderbook_rpc_v2, start_simple_market_maker_bot, stop_simple_market_maker_bot}; +use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, + stop_version_stat_collection, update_version_stat_collection}; use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc}; +use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}; use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc}; +use crate::rpc::lp_commands::db_id::get_shared_db_id; +use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc, + one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_liquidity_sources_rpc, + one_inch_v6_0_classic_swap_quote_rpc, + one_inch_v6_0_classic_swap_tokens_rpc}; +use crate::rpc::lp_commands::pubkey::*; +use crate::rpc::lp_commands::tokens::get_token_info; +use crate::rpc::lp_commands::tokens::{approve_token_rpc, get_token_allowance_rpc}; +use crate::rpc::lp_commands::trezor::trezor_connection_status; use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext}; -use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection, - stop_version_stat_collection, update_version_stat_collection}, - lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc}, - rpc::lp_commands::{get_public_key, get_public_key_hash, get_shared_db_id, trezor_connection_status}}; use coins::eth::EthCoin; use coins::my_tx_history_v2::my_tx_history_v2_rpc; +use coins::rpc_command::tendermint::staking::validators_rpc; use coins::rpc_command::tendermint::{ibc_chains, ibc_transfer_channels}; use coins::rpc_command::{account_balance::account_balance, get_current_mtp::get_current_mtp_rpc, @@ -159,6 +169,8 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, active_swaps_rpc).await, "add_delegation" => handle_mmrpc(ctx, request, add_delegation).await, "add_node_to_version_stat" => handle_mmrpc(ctx, request, add_node_to_version_stat).await, + "approve_token" => handle_mmrpc(ctx, request, approve_token_rpc).await, + "get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await, "best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await, "clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await, "enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::).await, @@ -184,6 +196,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, get_raw_transaction).await, "get_shared_db_id" => handle_mmrpc(ctx, request, get_shared_db_id).await, "get_staking_infos" => handle_mmrpc(ctx, request, get_staking_infos).await, + "get_token_info" => handle_mmrpc(ctx, request, get_token_info).await, "get_wallet_names" => handle_mmrpc(ctx, request, get_wallet_names_rpc).await, "max_maker_vol" => handle_mmrpc(ctx, request, max_maker_vol).await, "my_recent_swaps" => handle_mmrpc(ctx, request, my_recent_swaps_rpc).await, @@ -200,6 +213,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, start_version_stat_collection).await, "stop_simple_market_maker_bot" => handle_mmrpc(ctx, request, stop_simple_market_maker_bot).await, "stop_version_stat_collection" => handle_mmrpc(ctx, request, stop_version_stat_collection).await, + "tendermint_validators" => handle_mmrpc(ctx, request, validators_rpc).await, "trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await, "trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await, "update_nft" => handle_mmrpc(ctx, request, update_nft).await, @@ -217,6 +231,13 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult handle_mmrpc(ctx, request, set_swap_transaction_fee_policy).await, "send_asked_data" => handle_mmrpc(ctx, request, send_asked_data_rpc).await, "z_coin_tx_history" => handle_mmrpc(ctx, request, coins::my_tx_history_v2::z_coin_tx_history_rpc).await, + "1inch_v6_0_classic_swap_contract" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_contract_rpc).await, + "1inch_v6_0_classic_swap_quote" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_quote_rpc).await, + "1inch_v6_0_classic_swap_create" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_create_rpc).await, + "1inch_v6_0_classic_swap_liquidity_sources" => { + handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_liquidity_sources_rpc).await + }, + "1inch_v6_0_classic_swap_tokens" => handle_mmrpc(ctx, request, one_inch_v6_0_classic_swap_tokens_rpc).await, _ => MmError::err(DispatcherError::NoSuchMethod), } } diff --git a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs index 2415bc31ef..5f4b14f8b4 100644 --- a/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs +++ b/mm2src/mm2_main/src/rpc/dispatcher/dispatcher_legacy.rs @@ -7,7 +7,7 @@ use mm2_core::mm_ctx::MmArc; use serde_json::{self as json, Value as Json}; use std::net::SocketAddr; -use super::lp_commands_legacy::*; +use super::lp_commands::legacy::*; use crate::lp_ordermatch::{best_orders_rpc, buy, cancel_all_orders_rpc, cancel_order_rpc, my_orders, order_status, orderbook_depth_rpc, orderbook_rpc, orders_history_by_filter, sell, set_price, update_maker_order_rpc}; @@ -98,7 +98,6 @@ pub fn dispatcher(req: Json, ctx: MmArc) -> DispatcherRes { "order_status" => hyres(order_status(ctx, req)), "orderbook" => hyres(orderbook_rpc(ctx, req)), "orderbook_depth" => hyres(orderbook_depth_rpc(ctx, req)), - "sim_panic" => hyres(sim_panic(req)), "recover_funds_of_swap" => hyres(recover_funds_of_swap(ctx, req)), "sell" => hyres(sell(ctx, req)), "show_priv_key" => hyres(show_priv_key(ctx, req)), diff --git a/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs b/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs new file mode 100644 index 0000000000..29fa399bd0 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/db_id.rs @@ -0,0 +1,18 @@ +use crate::rpc::lp_commands::pubkey::GetPublicKeyError; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::mm_error::MmError; +use rpc::v1::types::H160 as H160Json; +use serde_json::Value as Json; + +pub type GetSharedDbIdResult = Result>; +pub type GetSharedDbIdError = GetPublicKeyError; + +#[derive(Serialize)] +pub struct GetSharedDbIdResponse { + shared_db_id: H160Json, +} + +pub async fn get_shared_db_id(ctx: MmArc, _req: Json) -> GetSharedDbIdResult { + let shared_db_id = ctx.shared_db_id().to_owned().into(); + Ok(GetSharedDbIdResponse { shared_db_id }) +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs b/mm2src/mm2_main/src/rpc/lp_commands/legacy.rs similarity index 94% rename from mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs rename to mm2src/mm2_main/src/rpc/lp_commands/legacy.rs index 59970c06bd..828e145f23 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands_legacy.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/legacy.rs @@ -31,7 +31,6 @@ use mm2_metrics::MetricsOps; use mm2_number::construct_detailed; use mm2_rpc::data::legacy::{BalanceResponse, CoinInitResponse, Mm2RpcResult, MmVersionResponse, Status}; use serde_json::{self as json, Value as Json}; -use std::borrow::Cow; use std::collections::HashSet; use uuid::Uuid; @@ -269,36 +268,6 @@ pub async fn stop(ctx: MmArc) -> Result>, String> { Ok(try_s!(Response::builder().body(res))) } -pub async fn sim_panic(req: Json) -> Result>, String> { - #[derive(Deserialize)] - struct Req { - #[serde(default)] - mode: String, - } - let req: Req = try_s!(json::from_value(req)); - - #[derive(Serialize)] - struct Ret<'a> { - /// Supported panic modes. - #[serde(skip_serializing_if = "Vec::is_empty")] - modes: Vec>, - } - let ret: Ret; - - if req.mode.is_empty() { - ret = Ret { - modes: vec!["simple".into()], - } - } else if req.mode == "simple" { - panic!("sim_panic: simple") - } else { - return ERR!("No such mode: {}", req.mode); - } - - let js = try_s!(json::to_vec(&ret)); - Ok(try_s!(Response::builder().body(js))) -} - pub fn version(ctx: MmArc) -> HyRes { match json::to_vec(&MmVersionResponse { result: ctx.mm_version.clone(), diff --git a/mm2src/mm2_main/src/rpc/lp_commands/mod.rs b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs new file mode 100644 index 0000000000..e61d5aead8 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod db_id; +pub mod legacy; +pub(crate) mod one_inch; +pub(crate) mod pubkey; +pub(crate) mod tokens; +pub(crate) mod trezor; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs new file mode 100644 index 0000000000..3d47853294 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch.rs @@ -0,0 +1,5 @@ +//! RPC implementation for integration with 1inch swap API provider. + +pub mod errors; +pub mod rpcs; +pub mod types; diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs new file mode 100644 index 0000000000..8ee65af984 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/errors.rs @@ -0,0 +1,99 @@ +use coins::{eth::u256_to_big_decimal, NumConversError}; +use common::{HttpStatusCode, StatusCode}; +use enum_derives::EnumFromStringify; +use mm2_number::BigDecimal; +use ser_error_derive::SerializeErrorType; +use serde::Serialize; +use trading_api::one_inch_api::errors::ApiClientError; + +#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)] +#[serde(tag = "error_type", content = "error_data")] +pub enum ApiIntegrationRpcError { + #[from_stringify("coins::CoinFindError")] + NoSuchCoin(String), + #[display(fmt = "EVM token needed")] + CoinTypeError, + #[display(fmt = "NFT not supported")] + NftNotSupported, + #[display(fmt = "Chain not supported")] + ChainNotSupported, + #[display(fmt = "Must be same chain")] + DifferentChains, + #[from_stringify("coins::UnexpectedDerivationMethod")] + MyAddressError(String), + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + #[display(fmt = "allowance not enough for 1inch contract, available: {allowance}, needed: {amount}")] + OneInchAllowanceNotEnough { + allowance: BigDecimal, + amount: BigDecimal, + }, + #[display(fmt = "1inch API error: {}", _0)] + OneInchError(ApiClientError), + ApiDataError(String), +} + +impl HttpStatusCode for ApiIntegrationRpcError { + fn status_code(&self) -> StatusCode { + match self { + ApiIntegrationRpcError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + ApiIntegrationRpcError::CoinTypeError + | ApiIntegrationRpcError::NftNotSupported + | ApiIntegrationRpcError::ChainNotSupported + | ApiIntegrationRpcError::DifferentChains + | ApiIntegrationRpcError::MyAddressError(_) + | ApiIntegrationRpcError::InvalidParam(_) + | ApiIntegrationRpcError::OutOfBounds { .. } + | ApiIntegrationRpcError::OneInchAllowanceNotEnough { .. } => StatusCode::BAD_REQUEST, + ApiIntegrationRpcError::OneInchError(_) | ApiIntegrationRpcError::ApiDataError(_) => { + StatusCode::BAD_GATEWAY + }, + } + } +} + +impl ApiIntegrationRpcError { + pub(crate) fn from_api_error(error: ApiClientError, decimals: Option) -> Self { + match error { + ApiClientError::InvalidParam(error) => ApiIntegrationRpcError::InvalidParam(error), + ApiClientError::OutOfBounds { param, value, min, max } => { + ApiIntegrationRpcError::OutOfBounds { param, value, min, max } + }, + ApiClientError::TransportError(_) + | ApiClientError::ParseBodyError { .. } + | ApiClientError::GeneralApiError { .. } => ApiIntegrationRpcError::OneInchError(error), + ApiClientError::AllowanceNotEnough { allowance, amount, .. } => { + ApiIntegrationRpcError::OneInchAllowanceNotEnough { + allowance: u256_to_big_decimal(allowance, decimals.unwrap_or_default()).unwrap_or_default(), + amount: u256_to_big_decimal(amount, decimals.unwrap_or_default()).unwrap_or_default(), + } + }, + } + } +} + +/// Error aggregator for errors of conversion of api returned values +#[derive(Debug, Display, Serialize)] +pub(crate) struct FromApiValueError(String); + +impl From for FromApiValueError { + fn from(err: NumConversError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: primitive_types::Error) -> Self { Self(format!("{:?}", err)) } +} + +impl From for FromApiValueError { + fn from(err: hex::FromHexError) -> Self { Self(err.to_string()) } +} + +impl From for FromApiValueError { + fn from(err: ethereum_types::FromDecStrErr) -> Self { Self(err.to_string()) } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs new file mode 100644 index 0000000000..a0c384463d --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/rpcs.rs @@ -0,0 +1,439 @@ +use super::errors::ApiIntegrationRpcError; +use super::types::{AggregationContractRequest, ClassicSwapCreateRequest, ClassicSwapLiquiditySourcesRequest, + ClassicSwapLiquiditySourcesResponse, ClassicSwapQuoteRequest, ClassicSwapResponse, + ClassicSwapTokensRequest, ClassicSwapTokensResponse}; +use coins::eth::{display_eth_address, wei_from_big_decimal, EthCoin, EthCoinType}; +use coins::{lp_coinfind_or_err, CoinWithDerivationMethod, MmCoin, MmCoinEnum}; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use trading_api::one_inch_api::client::ApiClient; +use trading_api::one_inch_api::types::{ClassicSwapCreateParams, ClassicSwapQuoteParams, ProtocolsResponse, + TokensResponse}; + +/// "1inch_v6_0_classic_swap_contract" rpc impl +/// used to get contract address (for e.g. to approve funds) +pub async fn one_inch_v6_0_classic_swap_contract_rpc( + _ctx: MmArc, + _req: AggregationContractRequest, +) -> MmResult { + Ok(ApiClient::classic_swap_contract().to_owned()) +} + +/// "1inch_classic_swap_quote" rpc impl +pub async fn one_inch_v6_0_classic_swap_quote_rpc( + ctx: MmArc, + req: ClassicSwapQuoteRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let query_params = ClassicSwapQuoteParams::new(base_contract, rel_contract, sell_amount.to_string()) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let quote = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_quote_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(quote, rel.decimals()) // use 'rel' as quote value is in the dst coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_classic_swap_create" rpc implementation +/// This rpc actually returns a transaction to call the 1inch swap aggregation contract. GUI should sign it and send to the chain. +/// We don't verify the transaction in any way and trust the 1inch api. +pub async fn one_inch_v6_0_classic_swap_create_rpc( + ctx: MmArc, + req: ClassicSwapCreateRequest, +) -> MmResult { + let (base, base_contract) = get_coin_for_one_inch(&ctx, &req.base).await?; + let (rel, rel_contract) = get_coin_for_one_inch(&ctx, &req.rel).await?; + api_supports_pair(&base, &rel)?; + let sell_amount = wei_from_big_decimal(&req.amount.to_decimal(), base.decimals()) + .mm_err(|err| ApiIntegrationRpcError::InvalidParam(err.to_string()))?; + let single_address = base.derivation_method().single_addr_or_err().await?; + + let query_params = ClassicSwapCreateParams::new( + base_contract, + rel_contract, + sell_amount.to_string(), + display_eth_address(&single_address), + req.slippage, + ) + .with_fee(req.fee) + .with_protocols(req.protocols) + .with_gas_price(req.gas_price) + .with_complexity_level(req.complexity_level) + .with_parts(req.parts) + .with_main_route_parts(req.main_route_parts) + .with_gas_limit(req.gas_limit) + .with_include_tokens_info(Some(req.include_tokens_info)) + .with_include_protocols(Some(req.include_protocols)) + .with_include_gas(Some(req.include_gas)) + .with_connector_tokens(req.connector_tokens) + .with_excluded_protocols(req.excluded_protocols) + .with_permit(req.permit) + .with_compatibility(req.compatibility) + .with_receiver(req.receiver) + .with_referrer(req.referrer) + .with_disable_estimate(req.disable_estimate) + .with_allow_partial_fill(req.allow_partial_fill) + .with_use_permit2(req.use_permit2) + .build_query_params() + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; + let swap_with_tx = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))? + .call_swap_api( + base.chain_id(), + ApiClient::get_swap_method().to_owned(), + Some(query_params), + ) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, Some(base.decimals())))?; // use 'base' as amount in errors is in the src coin + ClassicSwapResponse::from_api_classic_swap_data(swap_with_tx, base.decimals()) // use 'base' as we spend in the src coin + .mm_err(|err| ApiIntegrationRpcError::ApiDataError(err.to_string())) +} + +/// "1inch_v6_0_classic_swap_liquidity_sources" rpc implementation. +/// Returns list of DEX available for routing with the 1inch Aggregation contract +pub async fn one_inch_v6_0_classic_swap_liquidity_sources_rpc( + ctx: MmArc, + req: ClassicSwapLiquiditySourcesRequest, +) -> MmResult { + let response: ProtocolsResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_liquidity_sources_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapLiquiditySourcesResponse { + protocols: response.protocols, + }) +} + +/// "1inch_classic_swap_tokens" rpc implementation. +/// Returns list of tokens available for 1inch classic swaps +pub async fn one_inch_v6_0_classic_swap_tokens_rpc( + ctx: MmArc, + req: ClassicSwapTokensRequest, +) -> MmResult { + let response: TokensResponse = ApiClient::new(ctx) + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))? + .call_swap_api(req.chain_id, ApiClient::get_tokens_method().to_owned(), None) + .await + .mm_err(|api_err| ApiIntegrationRpcError::from_api_error(api_err, None))?; + Ok(ClassicSwapTokensResponse { + tokens: response.tokens, + }) +} + +async fn get_coin_for_one_inch(ctx: &MmArc, ticker: &str) -> MmResult<(EthCoin, String), ApiIntegrationRpcError> { + let coin = match lp_coinfind_or_err(ctx, ticker).await? { + MmCoinEnum::EthCoin(coin) => coin, + _ => return Err(MmError::new(ApiIntegrationRpcError::CoinTypeError)), + }; + let contract = match coin.coin_type { + EthCoinType::Eth => ApiClient::eth_special_contract().to_owned(), + EthCoinType::Erc20 { token_addr, .. } => display_eth_address(&token_addr), + EthCoinType::Nft { .. } => return Err(MmError::new(ApiIntegrationRpcError::NftNotSupported)), + }; + Ok((coin, contract)) +} + +#[allow(clippy::result_large_err)] +fn api_supports_pair(base: &EthCoin, rel: &EthCoin) -> MmResult<(), ApiIntegrationRpcError> { + if !ApiClient::is_chain_supported(base.chain_id()) { + return MmError::err(ApiIntegrationRpcError::ChainNotSupported); + } + if base.chain_id() != rel.chain_id() { + return MmError::err(ApiIntegrationRpcError::DifferentChains); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::rpc::lp_commands::one_inch::{rpcs::{one_inch_v6_0_classic_swap_create_rpc, + one_inch_v6_0_classic_swap_quote_rpc}, + types::{ClassicSwapCreateRequest, ClassicSwapQuoteRequest}}; + use coins::eth::EthCoin; + use coins_activation::platform_for_tests::init_platform_coin_with_tokens_loop; + use common::block_on; + use crypto::CryptoCtx; + use mm2_core::mm_ctx::MmCtxBuilder; + use mm2_number::{BigDecimal, MmNumber}; + use mocktopus::mocking::{MockResult, Mockable}; + use std::str::FromStr; + use trading_api::one_inch_api::{client::ApiClient, types::ClassicSwapData}; + + #[test] + fn test_classic_swap_response_conversion() { + let ticker_coin = "ETH".to_owned(); + let ticker_token = "JST".to_owned(); + let eth_conf = json!({ + "coin": ticker_coin, + "name": "ethereum", + "derivation_path": "m/44'/1'", + "chain_id": 1, + "protocol": { + "type": "ETH" + }, + "trezor_coin": "Ethereum" + }); + let jst_conf = json!({ + "coin": ticker_token, + "name": "jst", + "chain_id": 1, + "protocol": { + "type": "ERC20", + "protocol_data": { + "platform": "ETH", + "contract_address": "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19" + } + }, + }); + + let conf = json!({ + "coins": [eth_conf, jst_conf], + "1inch_api": "https://api.1inch.dev" + }); + let ctx = MmCtxBuilder::new().with_conf(conf).into_mm_arc(); + CryptoCtx::init_with_iguana_passphrase(ctx.clone(), "123").unwrap(); + + block_on(init_platform_coin_with_tokens_loop::( + ctx.clone(), + serde_json::from_value(json!({ + "ticker": ticker_coin, + "rpc_mode": "Default", + "nodes": [ + {"url": "https://rpc2.sepolia.org"}, + {"url": "https://rpc.sepolia.org/"} + ], + "swap_contract_address": "0xeA6D65434A15377081495a9E7C5893543E7c32cB", + "erc20_tokens_requests": [{"ticker": ticker_token}], + "priv_key_policy": "ContextPrivKey" + })) + .unwrap(), + )) + .unwrap(); + + let response_quote_raw = json!({ + "dstAmount": "13", + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Test just token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://example.org/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:JSTT", + "PEG:JST", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "SUSHI", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9" + } + ], + [ + { + "name": "ONE_INCH_LIMIT_ORDER_V3", + "part": 100, + "fromTokenAddress": "0xf16e81dce15b08f326220742020379b855b87df9", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ], + "gas": 452704 + }); + + let response_create_raw = json!({ + "dstAmount": "13", + "tx": { + "from": "0x590559f6fb7720f24ff3e2fccf6015b466e9c92c", + "to": "0x111111125421ca6dc452d289314280a0f8842a65", + "data": "0x07ed23790000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec70000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000590559f6fb7720f24ff3e2fccf6015b466e9c92c0000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000648e8755f7ac30b5e4fa3f9c00e2cb6667501797b8bc01a7a367a4b2889ca6a05d9c31a31a781c12a4c3bdfc2ef1e02942e388b6565989ebe860bd67925bda74fbe0000000000000000000000000000000000000000000000000005ea0005bc00a007e5c0d200000000000000000000000000000000059800057e00018500009500001a4041c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2d0e30db00c20c02aaa39b223fe8d0a0e5c4f27ead9083c756cc27b73644935b8e68019ac6356c40661e1bc3158606ae4071118002dc6c07b73644935b8e68019ac6356c40661e1bc3158600000000000000000000000000000000000000000000000000294932ccadc9c58c02aaa39b223fe8d0a0e5c4f27ead9083c756cc251204dff5675ecff96b565ba3804dd4a63799ccba406761d38e5ddf6ccf6cf7c55759d5210750b5d60f30044e331d039000000000000000000000000761d38e5ddf6ccf6cf7c55759d5210750b5d60f3000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002f8a744a79be00000000000000000000000042f527f50f16a103b6ccab48bccca214500c10210000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec00a0860a32ec00000000000000000000000000000000000000000000000000003005635d54300003d05120ead050515e10fdb3540ccd6f8236c46790508a76111111111117dc0aa78b770fa6a738034120c30200c4e525b10b000000000000000000000000000000000000000000000000000000000000002000000000000000000000000022b1a53ac4be63cdc1f47c99572290eff1edd8020000000000000000000000006a32cc044dd6359c27bb66e7b02dce6dd0fda2470000000000000000000000005f515f6c524b18ca30f7783fb58dd4be2e9904ec000000000000000000000000111111111117dc0aa78b770fa6a738034120c302000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003005635d5430000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000067138e8c00000000000000000000000000000000000000000000000000030fb9b1525d8185f8d63fbcbe42e5999263c349cb5d81000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000026000000000000000000000000067297ee4eb097e072b4ab6f1620268061ae8046400000000000000000000000060cba82ddbf4b5ddcd4398cdd05354c6a790c309000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002e0000000000000000000000000000000000000000000000000000000000000036000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041d26038ef66344af785ff342b86db3da06c4cc6a62f0ca80ffd78affc0a95ccad44e814acebb1deda729bbfe3050bec14a47af487cc1cadc75f43db2d073016c31c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a66cd52a747c5f60b9db637ffe30d0e413ec87858101832b4c5c1ae154bf247f3717c8ed4133e276ddf68d43a827f280863c91d6c42bc6ad1ec7083b2315b6fd1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020d6bdbf78dac17f958d2ee523a2206206994597c13d831ec780a06c4eca27dac17f958d2ee523a2206206994597c13d831ec7111111125421ca6dc452d289314280a0f8842a65000000000000000000000000000000000000000000000000c095c0a2", + "value": "10000000", + "gas": 721429, + "gasPrice": "9525172167" + }, + "srcToken": { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "symbol": ticker_coin, + "name": "Ether", + "decimals": 18, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png", + "tags": [ + "crosschain", + "GROUP:ETH", + "native", + "PEG:ETH" + ] + }, + "dstToken": { + "address": "0x1234567890123456789012345678901234567890", + "symbol": ticker_token, + "name": "Just Token", + "decimals": 6, + "eip2612": false, + "isFoT": false, + "logoURI": "https://tokens.1inch.io/0x1234567890123456789012345678901234567890.png", + "tags": [ + "crosschain", + "GROUP:USDT", + "PEG:USD", + "tokens" + ] + }, + "protocols": [ + [ + [ + { + "name": "UNISWAP_V2", + "part": 100, + "fromTokenAddress": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "toTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3" + } + ], + [ + { + "name": "ONE_INCH_LP_1_1", + "part": 100, + "fromTokenAddress": "0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3", + "toTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302" + } + ], + [ + { + "name": "PMM11", + "part": 100, + "fromTokenAddress": "0x111111111117dc0aa78b770fa6a738034120c302", + "toTokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7" + } + ] + ] + ] + }); + + let quote_req = ClassicSwapQuoteRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + }; + + let create_req = ClassicSwapCreateRequest { + base: ticker_coin.clone(), + rel: ticker_token.clone(), + amount: MmNumber::from("1.0"), + fee: None, + protocols: None, + gas_price: None, + complexity_level: None, + parts: None, + main_route_parts: None, + gas_limit: None, + include_tokens_info: true, + include_protocols: true, + include_gas: true, + connector_tokens: None, + slippage: 0.0, + excluded_protocols: None, + permit: None, + compatibility: None, + receiver: None, + referrer: None, + disable_estimate: None, + allow_partial_fill: None, + use_permit2: None, + }; + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_quote_raw = response_quote_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_quote_raw).unwrap()) + })) + }); + + let quote_response = block_on(one_inch_v6_0_classic_swap_quote_rpc(ctx.clone(), quote_req)).unwrap(); + assert_eq!( + quote_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(quote_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(quote_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(quote_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(quote_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(quote_response.gas.unwrap(), 452704_u128); + + ApiClient::call_swap_api::.mock_safe(move |_, _, _, _| { + let response_create_raw = response_create_raw.clone(); + MockResult::Return(Box::pin(async move { + Ok(serde_json::from_value::(response_create_raw).unwrap()) + })) + }); + let create_response = block_on(one_inch_v6_0_classic_swap_create_rpc(ctx, create_req)).unwrap(); + assert_eq!( + create_response.dst_amount.amount, + BigDecimal::from_str("0.000000000000000013").unwrap() + ); + assert_eq!(create_response.src_token.as_ref().unwrap().symbol, ticker_coin); + assert_eq!(create_response.src_token.as_ref().unwrap().decimals, 18); + assert_eq!(create_response.dst_token.as_ref().unwrap().symbol, ticker_token); + assert_eq!(create_response.dst_token.as_ref().unwrap().decimals, 6); + assert_eq!(create_response.tx.as_ref().unwrap().data.len(), 1960); + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs new file mode 100644 index 0000000000..202eb0dcf2 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/one_inch/types.rs @@ -0,0 +1,213 @@ +use crate::rpc::lp_commands::one_inch::errors::FromApiValueError; +use coins::eth::{u256_to_big_decimal, wei_to_gwei_decimal}; +use common::true_f; +use ethereum_types::{Address, U256}; +use mm2_err_handle::prelude::*; +use mm2_number::{construct_detailed, BigDecimal, MmNumber}; +use rpc::v1::types::Bytes as BytesJson; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use trading_api::one_inch_api::{self, + types::{ProtocolImage, ProtocolInfo, TokenInfo}}; + +construct_detailed!(DetailedAmount, amount); + +#[derive(Clone, Debug, Deserialize)] +pub struct AggregationContractRequest {} + +/// Request to get quote for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/quote_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapQuoteRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in return value (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, +} + +/// Request to create transaction for 1inch classic swap. +/// See 1inch docs for more details: https://portal.1inch.dev/documentation/apis/swap/classic-swap/Parameter%20Descriptions/swap_params +#[derive(Clone, Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ClassicSwapCreateRequest { + /// Base coin ticker + pub base: String, + /// Rel coin ticker + pub rel: String, + /// Swap amount in coins (with fraction) + pub amount: MmNumber, + /// Allowed slippage, min: 0; max: 50 + pub slippage: f32, + /// Partner fee, percentage of src token amount will be sent to referrer address, min: 0; max: 3. + /// Should be the same for quote and swap rpc. Default is 0 + pub fee: Option, + /// Specify liquidity sources + /// e.g.: &protocols=WETH,CURVE,BALANCER,...,ZRX + /// (by default - all used) + pub protocols: Option, + /// Network price per gas, in Gwei for this rpc. + /// 1inch takes in account gas expenses to determine exchange route. Should be the same for a quote and swap. + /// If not set the 'fast' network gas price will be used + pub gas_price: Option, + /// Maximum number of token-connectors to be used in a transaction, min: 0; max: 3; default: 2 + pub complexity_level: Option, + /// Limit maximum number of parts each main route parts can be split into. + /// Should be the same for a quote and swap. Default: 20; max: 100 + pub parts: Option, + /// Limit maximum number of main route parts. Should be the same for a quote and swap. Default: 20; max: 50; + pub main_route_parts: Option, + /// Maximum amount of gas for a swap. + /// Should be the same for a quote and swap. Default: 11500000; max: 11500000 + pub gas_limit: Option, + /// Return fromToken and toToken info in response (default is true) + #[serde(default = "true_f")] + pub include_tokens_info: bool, + /// Return used swap protocols in response (default is true) + #[serde(default = "true_f")] + pub include_protocols: bool, + /// Include estimated gas in response (default is true) + #[serde(default = "true_f")] + pub include_gas: bool, + /// Token-connectors can be specified via this parameter. If not set, default token-connectors will be used + pub connector_tokens: Option, + /// Excluded supported liquidity sources. Should be the same for a quote and swap, max: 5 + pub excluded_protocols: Option, + /// Used according https://eips.ethereum.org/EIPS/eip-2612 + pub permit: Option, + /// Exclude the Unoswap method + pub compatibility: Option, + /// This address will receive funds after the swap. By default same address as 'my address' + pub receiver: Option, + /// Address to receive the partner fee. Must be set explicitly if fee is also set + pub referrer: Option, + /// if true, disable most of the checks, default: false + pub disable_estimate: Option, + /// if true, the algorithm can cancel part of the route, if the rate has become less attractive. + /// Unswapped tokens will return to 'my address'. Default: true + pub allow_partial_fill: Option, + /// Enable this flag for auto approval by Permit2 contract if you did an approval to Uniswap Permit2 smart contract for this token. + /// Default is false + pub use_permit2: Option, +} + +/// Response for both classic swap quote or create swap calls +#[derive(Serialize, Debug)] +pub struct ClassicSwapResponse { + /// Destination token amount, in coins (with fraction) + pub dst_amount: DetailedAmount, + /// Source (base) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub src_token: Option, + /// Destination (rel) token info + #[serde(skip_serializing_if = "Option::is_none")] + pub dst_token: Option, + /// Used liquidity sources + #[serde(skip_serializing_if = "Option::is_none")] + pub protocols: Option>>>, + /// Swap tx fields (returned only for create swap rpc) + #[serde(skip_serializing_if = "Option::is_none")] + pub tx: Option, + /// Estimated (returned only for quote rpc) + pub gas: Option, +} + +impl ClassicSwapResponse { + pub(crate) fn from_api_classic_swap_data( + data: one_inch_api::types::ClassicSwapData, + decimals: u8, + ) -> MmResult { + Ok(Self { + dst_amount: MmNumber::from(u256_to_big_decimal(U256::from_dec_str(&data.dst_amount)?, decimals)?).into(), + src_token: data.src_token, + dst_token: data.dst_token, + protocols: data.protocols, + tx: data + .tx + .map(|tx| TxFields::from_api_tx_fields(tx, decimals)) + .transpose()?, + gas: data.gas, + }) + } +} + +#[derive(Serialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: BytesJson, + pub value: BigDecimal, + /// Estimated gas price in gwei + pub gas_price: BigDecimal, + pub gas: u128, // TODO: in eth EthTxFeeDetails rpc we use u64. Better have identical u128 everywhere +} + +impl TxFields { + pub(crate) fn from_api_tx_fields( + tx_fields: one_inch_api::types::TxFields, + decimals: u8, + ) -> MmResult { + Ok(Self { + from: tx_fields.from, + to: tx_fields.to, + data: BytesJson::from(hex::decode(str_strip_0x!(tx_fields.data.as_str()))?), + value: u256_to_big_decimal(U256::from_dec_str(&tx_fields.value)?, decimals)?, + gas_price: wei_to_gwei_decimal(U256::from_dec_str(&tx_fields.gas_price)?)?, + gas: tx_fields.gas, + }) + } +} + +#[derive(Deserialize)] +pub struct ClassicSwapLiquiditySourcesRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapLiquiditySourcesResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct ClassicSwapTokensRequest { + pub chain_id: u64, +} + +#[derive(Serialize)] +pub struct ClassicSwapTokensResponse { + pub tokens: HashMap, +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs b/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs new file mode 100644 index 0000000000..f5a5a95063 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/pubkey.rs @@ -0,0 +1,48 @@ +use common::HttpStatusCode; +use crypto::{CryptoCtx, CryptoCtxError}; +use derive_more::Display; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::prelude::*; +use rpc::v1::types::H160 as H160Json; +use serde_json::Value as Json; + +pub type GetPublicKeyRpcResult = Result>; + +#[derive(Serialize, Display, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum GetPublicKeyError { + Internal(String), +} + +impl From for GetPublicKeyError { + fn from(_: CryptoCtxError) -> Self { GetPublicKeyError::Internal("public_key not available".to_string()) } +} + +#[derive(Serialize)] +pub struct GetPublicKeyResponse { + public_key: String, +} + +impl HttpStatusCode for GetPublicKeyError { + fn status_code(&self) -> StatusCode { + match self { + GetPublicKeyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +pub async fn get_public_key(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { + let public_key = CryptoCtx::from_ctx(&ctx)?.mm2_internal_pubkey().to_string(); + Ok(GetPublicKeyResponse { public_key }) +} + +#[derive(Serialize)] +pub struct GetPublicKeyHashResponse { + public_key_hash: H160Json, +} + +pub async fn get_public_key_hash(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { + let public_key_hash = ctx.rmd160().to_owned().into(); + Ok(GetPublicKeyHashResponse { public_key_hash }) +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs new file mode 100644 index 0000000000..78697530c1 --- /dev/null +++ b/mm2src/mm2_main/src/rpc/lp_commands/tokens.rs @@ -0,0 +1,173 @@ +//! This source file is for RPCs specific for EVM platform +use coins::eth::erc20::{get_erc20_ticker_by_contract_address, get_erc20_token_info, Erc20TokenInfo}; +use coins::eth::valid_addr_from_str; +use coins::eth::{u256_to_big_decimal, wei_from_big_decimal, EthCoin, Web3RpcError}; +use coins::{lp_coinfind_or_err, CoinFindError, CoinProtocol, MmCoin, MmCoinEnum, NumConversError, Transaction, + TransactionErr}; +use common::HttpStatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::Address as EthAddress; +use futures::compat::Future01CompatExt; +use http::StatusCode; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_to_mm::MapToMmResult, mm_error::MmError, prelude::MmResult}; +use mm2_number::BigDecimal; + +#[derive(Deserialize)] +pub struct TokenInfoRequest { + protocol: CoinProtocol, +} + +#[derive(Serialize)] +#[serde(tag = "type", content = "info")] +pub enum TokenInfo { + ERC20(Erc20TokenInfo), +} + +#[derive(Serialize)] +pub struct TokenInfoResponse { + #[serde(skip_serializing_if = "Option::is_none")] + config_ticker: Option, + #[serde(flatten)] + info: TokenInfo, +} + +#[derive(Debug, Deserialize, Display, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum TokenInfoError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Custom tokens are not supported for {} protocol yet!", protocol)] + UnsupportedTokenProtocol { protocol: String }, + #[display(fmt = "Invalid request {}", _0)] + InvalidRequest(String), + #[display(fmt = "Error retrieving token info {}", _0)] + RetrieveInfoError(String), +} + +impl HttpStatusCode for TokenInfoError { + fn status_code(&self) -> StatusCode { + match self { + TokenInfoError::NoSuchCoin { .. } => StatusCode::NOT_FOUND, + TokenInfoError::UnsupportedTokenProtocol { .. } | TokenInfoError::InvalidRequest(_) => { + StatusCode::BAD_REQUEST + }, + TokenInfoError::RetrieveInfoError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl From for TokenInfoError { + fn from(e: CoinFindError) -> Self { + match e { + CoinFindError::NoSuchCoin { coin } => TokenInfoError::NoSuchCoin { coin }, + } + } +} + +pub async fn get_token_info(ctx: MmArc, req: TokenInfoRequest) -> MmResult { + // Check that the protocol is a token protocol + let platform = req.protocol.platform().ok_or(TokenInfoError::InvalidRequest(format!( + "Protocol '{:?}' is not a token protocol", + req.protocol + )))?; + // Platform coin should be activated + let platform_coin = lp_coinfind_or_err(&ctx, platform).await?; + match platform_coin { + MmCoinEnum::EthCoin(eth_coin) => { + let contract_address_str = + req.protocol + .contract_address() + .ok_or(TokenInfoError::UnsupportedTokenProtocol { + protocol: platform.to_string(), + })?; + let contract_address = valid_addr_from_str(contract_address_str).map_to_mm(|e| { + let error = format!("Invalid contract address: {}", e); + TokenInfoError::InvalidRequest(error) + })?; + + let config_ticker = get_erc20_ticker_by_contract_address(&ctx, platform, contract_address_str); + let token_info = get_erc20_token_info(ð_coin, contract_address) + .await + .map_to_mm(TokenInfoError::RetrieveInfoError)?; + Ok(TokenInfoResponse { + config_ticker, + info: TokenInfo::ERC20(token_info), + }) + }, + _ => MmError::err(TokenInfoError::UnsupportedTokenProtocol { + protocol: platform.to_string(), + }), + } +} + +#[derive(Debug, Deserialize, Display, EnumFromStringify, Serialize, SerializeErrorType)] +#[serde(tag = "error_type", content = "error_data")] +pub enum Erc20CallError { + #[display(fmt = "No such coin {}", coin)] + NoSuchCoin { coin: String }, + #[display(fmt = "Coin not supported {}", coin)] + CoinNotSupported { coin: String }, + #[from_stringify("NumConversError")] + #[display(fmt = "Invalid param: {}", _0)] + InvalidParam(String), + #[from_stringify("TransactionErr")] + #[display(fmt = "Transaction error {}", _0)] + TransactionError(String), + #[from_stringify("Web3RpcError")] + #[display(fmt = "Web3 RPC error {}", _0)] + Web3RpcError(String), +} + +impl HttpStatusCode for Erc20CallError { + fn status_code(&self) -> StatusCode { + match self { + Erc20CallError::NoSuchCoin { .. } + | Erc20CallError::CoinNotSupported { .. } + | Erc20CallError::InvalidParam(_) => StatusCode::BAD_REQUEST, + Erc20CallError::TransactionError(_) | Erc20CallError::Web3RpcError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Erc20AllowanceRequest { + coin: String, + spender: EthAddress, +} + +/// Call allowance method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#approve). +/// Returns BigDecimal allowance value. +pub async fn get_token_allowance_rpc(ctx: MmArc, req: Erc20AllowanceRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let wei = eth_coin.allowance(req.spender).compat().await?; + let amount = u256_to_big_decimal(wei, eth_coin.decimals())?; + Ok(amount) +} + +#[derive(Debug, Deserialize)] +pub struct Erc20ApproveRequest { + coin: String, + spender: EthAddress, + amount: BigDecimal, +} + +/// Call approve method for ERC20 tokens (see https://eips.ethereum.org/EIPS/eip-20#allowance). +/// Returns approval transaction hash. +pub async fn approve_token_rpc(ctx: MmArc, req: Erc20ApproveRequest) -> MmResult { + let eth_coin = find_erc20_eth_coin(&ctx, &req.coin).await?; + let amount = wei_from_big_decimal(&req.amount, eth_coin.decimals())?; + let tx = eth_coin.approve(req.spender, amount).compat().await?; + Ok(format!("0x{:02x}", tx.tx_hash_as_bytes())) +} + +async fn find_erc20_eth_coin(ctx: &MmArc, coin: &str) -> Result> { + match lp_coinfind_or_err(ctx, coin).await { + Ok(MmCoinEnum::EthCoin(eth_coin)) => Ok(eth_coin), + Ok(_) => Err(MmError::new(Erc20CallError::CoinNotSupported { + coin: coin.to_string(), + })), + Err(_) => Err(MmError::new(Erc20CallError::NoSuchCoin { coin: coin.to_string() })), + } +} diff --git a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs b/mm2src/mm2_main/src/rpc/lp_commands/trezor.rs similarity index 52% rename from mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs rename to mm2src/mm2_main/src/rpc/lp_commands/trezor.rs index ae992c6d3e..16698eb3cc 100644 --- a/mm2src/mm2_main/src/rpc/lp_commands/lp_commands.rs +++ b/mm2src/mm2_main/src/rpc/lp_commands/trezor.rs @@ -1,63 +1,9 @@ use common::HttpStatusCode; use crypto::{CryptoCtx, CryptoCtxError, HwConnectionStatus, HwPubkey}; -use derive_more::Display; use http::StatusCode; use mm2_core::mm_ctx::MmArc; -use mm2_err_handle::prelude::*; -use rpc::v1::types::H160 as H160Json; -use serde_json::Value as Json; - -pub type GetPublicKeyRpcResult = Result>; -pub type GetSharedDbIdResult = Result>; -pub type GetSharedDbIdError = GetPublicKeyError; - -#[derive(Serialize, Display, SerializeErrorType)] -#[serde(tag = "error_type", content = "error_data")] -pub enum GetPublicKeyError { - Internal(String), -} - -impl From for GetPublicKeyError { - fn from(_: CryptoCtxError) -> Self { GetPublicKeyError::Internal("public_key not available".to_string()) } -} - -#[derive(Serialize)] -pub struct GetPublicKeyResponse { - public_key: String, -} - -impl HttpStatusCode for GetPublicKeyError { - fn status_code(&self) -> StatusCode { - match self { - GetPublicKeyError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -pub async fn get_public_key(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { - let public_key = CryptoCtx::from_ctx(&ctx)?.mm2_internal_pubkey().to_string(); - Ok(GetPublicKeyResponse { public_key }) -} - -#[derive(Serialize)] -pub struct GetPublicKeyHashResponse { - public_key_hash: H160Json, -} - -pub async fn get_public_key_hash(ctx: MmArc, _req: Json) -> GetPublicKeyRpcResult { - let public_key_hash = ctx.rmd160().to_owned().into(); - Ok(GetPublicKeyHashResponse { public_key_hash }) -} - -#[derive(Serialize)] -pub struct GetSharedDbIdResponse { - shared_db_id: H160Json, -} - -pub async fn get_shared_db_id(ctx: MmArc, _req: Json) -> GetSharedDbIdResult { - let shared_db_id = ctx.shared_db_id().to_owned().into(); - Ok(GetSharedDbIdResponse { shared_db_id }) -} +use mm2_err_handle::mm_error::{MmError, MmResult}; +use mm2_err_handle::or_mm_error::OrMmError; #[derive(Serialize, Display, SerializeErrorType)] #[serde(tag = "error_type", content = "error_data")] diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs index b3b711faa3..7286034775 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_common.rs @@ -41,10 +41,14 @@ pub use secp256k1::{PublicKey, SecretKey}; use serde_json::{self as json, Value as Json}; pub use std::cell::Cell; use std::process::{Command, Stdio}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use std::str::FromStr; pub use std::{env, thread}; -use std::{path::PathBuf, str::FromStr, sync::Mutex, time::Duration}; +use std::{path::PathBuf, sync::Mutex, time::Duration}; use testcontainers::{clients::Cli, core::WaitFor, Container, GenericImage, RunnableImage}; -use web3::types::{Address as EthAddress, BlockId, BlockNumber, TransactionRequest}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use web3::types::Address as EthAddress; +use web3::types::{BlockId, BlockNumber, TransactionRequest}; use web3::{transports::Http, Web3}; lazy_static! { @@ -65,10 +69,15 @@ lazy_static! { /// This approach addresses the `replacement transaction` issue, which occurs when different transactions share the same nonce. pub static ref MM_CTX1: MmArc = MmCtxBuilder::new().with_conf(json!({"use_trading_proto_v2": true})).into_mm_arc(); pub static ref GETH_WEB3: Web3 = Web3::new(Http::new(GETH_RPC_URL).unwrap()); - pub static ref SEPOLIA_WEB3: Web3 = Web3::new(Http::new(SEPOLIA_RPC_URL).unwrap()); // Mutex used to prevent nonce re-usage during funding addresses used in tests pub static ref GETH_NONCE_LOCK: Mutex<()> = Mutex::new(()); +} + +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +lazy_static! { + pub static ref SEPOLIA_WEB3: Web3 = Web3::new(Http::new(SEPOLIA_RPC_URL).unwrap()); pub static ref SEPOLIA_NONCE_LOCK: Mutex<()> = Mutex::new(()); + pub static ref SEPOLIA_TESTS_LOCK: Mutex<()> = Mutex::new(()); } pub static mut QICK_TOKEN_ADDRESS: Option = None; @@ -79,6 +88,7 @@ pub static mut QTUM_CONF_PATH: Option = None; pub static mut GETH_ACCOUNT: H160Eth = H160Eth::zero(); /// ERC20 token address on Geth dev node pub static mut GETH_ERC20_CONTRACT: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_ERC20_CONTRACT: H160Eth = H160Eth::zero(); /// Swap contract address on Geth dev node pub static mut GETH_SWAP_CONTRACT: H160Eth = H160Eth::zero(); @@ -86,7 +96,9 @@ pub static mut GETH_SWAP_CONTRACT: H160Eth = H160Eth::zero(); pub static mut GETH_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); /// Taker Swap V2 contract address on Geth dev node pub static mut GETH_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_TAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static mut SEPOLIA_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); /// Swap contract (with watchers support) address on Geth dev node pub static mut GETH_WATCHERS_SWAP_CONTRACT: H160Eth = H160Eth::zero(); @@ -96,9 +108,11 @@ pub static mut GETH_ERC721_CONTRACT: H160Eth = H160Eth::zero(); pub static mut GETH_ERC1155_CONTRACT: H160Eth = H160Eth::zero(); /// NFT Maker Swap V2 contract address on Geth dev node pub static mut GETH_NFT_MAKER_SWAP_V2: H160Eth = H160Eth::zero(); +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// NFT Maker Swap V2 contract address on Sepolia testnet pub static mut SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2: H160Eth = H160Eth::zero(); pub static GETH_RPC_URL: &str = "http://127.0.0.1:8545"; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub static SEPOLIA_RPC_URL: &str = "https://ethereum-sepolia-rpc.publicnode.com"; // use thread local to affect only the current running test @@ -1594,12 +1608,15 @@ pub fn init_geth_node() { thread::sleep(Duration::from_millis(100)); } - SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); - SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); - SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); - // TODO update this - SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); - + #[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] + { + SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 = + EthAddress::from_str("0x9eb88cd58605d8fb9b14652d6152727f7e95fb4d").unwrap(); + SEPOLIA_ERC20_CONTRACT = EthAddress::from_str("0xF7b5F8E8555EF7A743f24D3E974E23A3C6cB6638").unwrap(); + SEPOLIA_TAKER_SWAP_V2 = EthAddress::from_str("0x7Cc9F2c1c3B797D09B9d1CCd7FDcD2539a4b3874").unwrap(); + // deploy tx https://sepolia.etherscan.io/tx/0x6f743d79ecb806f5899a6a801083e33eba9e6f10726af0873af9f39883db7f11 + SEPOLIA_MAKER_SWAP_V2 = EthAddress::from_str("0xf9000589c66Df3573645B59c10aa87594Edc318F").unwrap(); + } let alice_passphrase = get_passphrase!(".env.client", "ALICE_PASSPHRASE").unwrap(); let alice_keypair = key_pair_from_seed(&alice_passphrase).unwrap(); let alice_eth_addr = addr_from_raw_pubkey(alice_keypair.public()).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs index 69ce1afb48..39e8af90cf 100644 --- a/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs +++ b/mm2src/mm2_main/tests/docker_tests/docker_tests_inner.rs @@ -17,15 +17,17 @@ use common::{block_on, block_on_f01, executor::Timer, get_utc_timestamp, now_sec use crypto::privkey::key_pair_from_seed; use crypto::{CryptoCtx, DerivationPath, KeyPairPolicy}; use http::StatusCode; +use mm2_libp2p::behaviours::atomicdex::MAX_TIME_GAP_FOR_CONNECTED_PEER; use mm2_number::{BigDecimal, BigRational, MmNumber}; use mm2_test_helpers::for_tests::{check_my_swap_status_amounts, disable_coin, disable_coin_err, enable_eth_coin, enable_eth_with_tokens_v2, erc20_dev_conf, eth_dev_conf, get_locked_amount, kmd_conf, max_maker_vol, mm_dump, mycoin1_conf, mycoin_conf, set_price, start_swaps, wait_for_swap_contract_negotiation, wait_for_swap_negotiation_failure, - MarketMakerIt, Mm2TestConf}; + MarketMakerIt, Mm2TestConf, DEFAULT_RPC_PASSWORD}; use mm2_test_helpers::{get_passphrase, structs::*}; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; use std::env; use std::iter::FromIterator; use std::str::FromStr; @@ -5279,127 +5281,6 @@ fn test_sell_min_volume_dust() { assert!(!rc.0.is_success(), "!sell: {}", rc.1); } -#[test] -fn test_enable_eth_erc20_coins_with_enable_hd() { - const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; - - let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); - let swap_contract = format!("0x{}", hex::encode(swap_contract())); - - // Withdraw from HD account 0, change address 0, index 0 - let path_to_address = HDAccountAddressId::default(); - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[0].address, - "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - block_on(mm_hd.stop()).unwrap(); - - // Enable HD account 0, change address 0, index 1 - let path_to_address = HDAccountAddressId { - account_id: 0, - chain: Bip44Chain::External, - address_id: 1, - }; - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[1].address, - "0xDe841899aB4A22E23dB21634e54920aDec402397" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - block_on(mm_hd.stop()).unwrap(); - - // Enable HD account 77, change address 0, index 7 - let path_to_address = HDAccountAddressId { - account_id: 77, - chain: Bip44Chain::External, - address_id: 7, - }; - let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); - let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); - let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); - log!("Alice log path: {}", mm_hd.log_path.display()); - - let eth_enable = block_on(enable_eth_with_tokens_v2( - &mm_hd, - "ETH", - &["ERC20DEV"], - &swap_contract, - &[GETH_RPC_URL], - 60, - Some(path_to_address), - )); - let activation_result = match eth_enable { - EthWithTokensActivationResult::HD(hd) => hd, - _ => panic!("Expected EthWithTokensActivationResult::HD"), - }; - let balance = match activation_result.wallet_balance { - EnableCoinBalanceMap::HD(hd) => hd, - _ => panic!("Expected EnableCoinBalance::HD"), - }; - let account = balance.accounts.get(0).expect("Expected account at index 0"); - assert_eq!( - account.addresses[7].address, - "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" - ); - assert_eq!(account.addresses[0].balance.len(), 2); - assert!(account.addresses[0].balance.contains_key("ETH")); - assert!(account.addresses[0].balance.contains_key("ERC20DEV")); - - block_on(mm_hd.stop()).unwrap(); -} - fn request_and_check_orderbook_depth(mm_alice: &MarketMakerIt) { let rc = block_on(mm_alice.rpc(&json! ({ "userpass": mm_alice.userpass, @@ -5539,3 +5420,157 @@ fn test_orderbook_depth() { block_on(mm_bob.stop()).unwrap(); block_on(mm_alice.stop()).unwrap(); } + +#[test] +fn test_approve_erc20() { + let privkey = random_secp256k1_secret(); + fill_eth_erc20_with_private_key(privkey); + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let mm = MarketMakerIt::start( + Mm2TestConf::seednode(&format!("0x{}", hex::encode(privkey)), &coins).conf, + DEFAULT_RPC_PASSWORD.to_string(), + None, + ) + .unwrap(); + + let (_mm_dump_log, _mm_dump_dashboard) = mm.mm_dump(); + log!("Node log path: {}", mm.log_path.display()); + + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + let _eth_enable = block_on(enable_eth_coin( + &mm, + "ETH", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + let _erc20_enable = block_on(enable_eth_coin( + &mm, + "ERC20DEV", + &[GETH_RPC_URL], + &swap_contract, + None, + false, + )); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"approve_token", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + "amount": BigDecimal::from_str("11.0").unwrap(), + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "approve_token error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert!( + hex::decode(str_strip_0x!(res["result"].as_str().unwrap())).is_ok(), + "approve_token result incorrect" + ); + + let rc = block_on(mm.rpc(&json!({ + "userpass": mm.userpass, + "method":"get_token_allowance", + "mmrpc":"2.0", + "id": 0, + "params":{ + "coin": "ERC20DEV", + "spender": swap_contract, + } + }))) + .unwrap(); + assert!(rc.0.is_success(), "get_token_allowance error: {}", rc.1); + let res = serde_json::from_str::(&rc.1).unwrap(); + assert_eq!( + BigDecimal::from_str(res["result"].as_str().unwrap()).unwrap(), + BigDecimal::from_str("11.0").unwrap(), + "get_token_allowance result incorrect" + ); + + block_on(mm.stop()).unwrap(); +} + +#[test] +fn test_peer_time_sync_validation() { + let timeoffset_tolerable = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() - 1; + let timeoffset_too_big = TryInto::::try_into(MAX_TIME_GAP_FOR_CONNECTED_PEER).unwrap() + 1; + + let start_peers_with_time_offset = |offset: i64| -> (Json, Json) { + let (_ctx, _, bob_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN", 10.into()); + let (_ctx, _, alice_priv_key) = generate_utxo_coin_with_random_privkey("MYCOIN1", 10.into()); + let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); + let bob_conf = Mm2TestConf::seednode(&hex::encode(bob_priv_key), &coins); + let mut mm_bob = block_on(MarketMakerIt::start_with_envs( + bob_conf.conf, + bob_conf.rpc_password, + None, + &[], + )) + .unwrap(); + let (_bob_dump_log, _bob_dump_dashboard) = mm_dump(&mm_bob.log_path); + block_on(mm_bob.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + let alice_conf = + Mm2TestConf::light_node(&hex::encode(alice_priv_key), &coins, &[mm_bob.ip.to_string().as_str()]); + let mut mm_alice = block_on(MarketMakerIt::start_with_envs( + alice_conf.conf, + alice_conf.rpc_password, + None, + &[("TEST_TIMESTAMP_OFFSET", offset.to_string().as_str())], + )) + .unwrap(); + let (_alice_dump_log, _alice_dump_dashboard) = mm_dump(&mm_alice.log_path); + block_on(mm_alice.wait_for_log(22., |log| log.contains(">>>>>>>>> DEX stats "))).unwrap(); + + let res_bob = block_on(mm_bob.rpc(&json!({ + "userpass": mm_bob.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!(res_bob.0.is_success(), "!get_directly_connected_peers: {}", res_bob.1); + let bob_peers = serde_json::from_str::(&res_bob.1).unwrap(); + + let res_alice = block_on(mm_alice.rpc(&json!({ + "userpass": mm_alice.userpass, + "method": "get_directly_connected_peers", + }))) + .unwrap(); + assert!( + res_alice.0.is_success(), + "!get_directly_connected_peers: {}", + res_alice.1 + ); + let alice_peers = serde_json::from_str::(&res_alice.1).unwrap(); + + block_on(mm_bob.stop()).unwrap(); + block_on(mm_alice.stop()).unwrap(); + (bob_peers, alice_peers) + }; + + // check with small time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_tolerable); + assert!( + bob_peers["result"].as_object().unwrap().len() == 1, + "bob must have one peer" + ); + assert!( + alice_peers["result"].as_object().unwrap().len() == 1, + "alice must have one peer" + ); + + // check with too big time offset: + let (bob_peers, alice_peers) = start_peers_with_time_offset(timeoffset_too_big); + assert!( + bob_peers["result"].as_object().unwrap().is_empty(), + "bob must have no peers" + ); + assert!( + alice_peers["result"].as_object().unwrap().is_empty(), + "alice must have no peers" + ); +} diff --git a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs index c0c93b22f1..dcb09e29cd 100644 --- a/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/eth_docker_tests.rs @@ -1,9 +1,11 @@ use super::docker_tests_common::{random_secp256k1_secret, ERC1155_TEST_ABI, ERC721_TEST_ABI, GETH_ACCOUNT, GETH_ERC1155_CONTRACT, GETH_ERC20_CONTRACT, GETH_ERC721_CONTRACT, GETH_MAKER_SWAP_V2, GETH_NFT_MAKER_SWAP_V2, GETH_NONCE_LOCK, GETH_RPC_URL, GETH_SWAP_CONTRACT, - GETH_TAKER_SWAP_V2, GETH_WATCHERS_SWAP_CONTRACT, GETH_WEB3, MM_CTX, MM_CTX1, - SEPOLIA_ERC20_CONTRACT, SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2, SEPOLIA_MAKER_SWAP_V2, - SEPOLIA_NONCE_LOCK, SEPOLIA_RPC_URL, SEPOLIA_TAKER_SWAP_V2, SEPOLIA_WEB3}; + GETH_TAKER_SWAP_V2, GETH_WATCHERS_SWAP_CONTRACT, GETH_WEB3, MM_CTX, MM_CTX1}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use super::docker_tests_common::{SEPOLIA_ERC20_CONTRACT, SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2, SEPOLIA_MAKER_SWAP_V2, + SEPOLIA_NONCE_LOCK, SEPOLIA_RPC_URL, SEPOLIA_TAKER_SWAP_V2, SEPOLIA_TESTS_LOCK, + SEPOLIA_WEB3}; use crate::common::Future01CompatExt; use bitcrypto::{dhash160, sha256}; use coins::eth::gas_limit::ETH_MAX_TRADE_GAS; @@ -11,32 +13,50 @@ use coins::eth::v2_activation::{eth_coin_from_conf_and_request_v2, EthActivation use coins::eth::{checksum_address, eth_addr_to_hex, eth_coin_from_conf_and_request, EthCoin, EthCoinType, EthPrivKeyBuildPolicy, SignedEthTx, SwapV2Contracts, ERC20_ABI}; use coins::nft::nft_structs::{Chain, ContractType, NftInfo}; -use coins::{lp_coinfind, CoinProtocol, CoinWithDerivationMethod, CoinsContext, CommonSwapOpsV2, ConfirmPaymentInput, - DerivationMethod, DexFee, Eip1559Ops, FoundSwapTxSpend, FundingTxSpend, GenTakerFundingSpendArgs, - GenTakerPaymentSpendArgs, MakerNftSwapOpsV2, MarketCoinOps, MmCoinEnum, MmCoinStruct, NftSwapInfo, - ParseCoinAssocTypes, ParseNftAssocTypes, PrivKeyBuildPolicy, RefundFundingSecretArgs, - RefundNftMakerPaymentArgs, RefundPaymentArgs, RefundTakerPaymentArgs, SearchForSwapTxSpendInput, - SendNftMakerPaymentArgs, SendPaymentArgs, SendTakerFundingArgs, SpendNftMakerPaymentArgs, - SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, TakerCoinSwapOpsV2, ToBytes, - Transaction, TxPreimageWithSig, ValidateNftMakerPaymentArgs, ValidateTakerFundingArgs}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use coins::{lp_coinfind, CoinsContext, DexFee, FundingTxSpend, GenTakerFundingSpendArgs, GenTakerPaymentSpendArgs, + MakerCoinSwapOpsV2, MmCoinEnum, MmCoinStruct, RefundFundingSecretArgs, RefundMakerPaymentSecretArgs, + RefundMakerPaymentTimelockArgs, RefundTakerPaymentArgs, SendMakerPaymentArgs, SendTakerFundingArgs, + SpendMakerPaymentArgs, TakerCoinSwapOpsV2, TxPreimageWithSig, ValidateMakerPaymentArgs, + ValidateTakerFundingArgs}; +use coins::{CoinProtocol, CoinWithDerivationMethod, CommonSwapOpsV2, ConfirmPaymentInput, DerivationMethod, + Eip1559Ops, FoundSwapTxSpend, MakerNftSwapOpsV2, MarketCoinOps, NftSwapInfo, ParseCoinAssocTypes, + ParseNftAssocTypes, PrivKeyBuildPolicy, RefundNftMakerPaymentArgs, RefundPaymentArgs, + SearchForSwapTxSpendInput, SendNftMakerPaymentArgs, SendPaymentArgs, SpendNftMakerPaymentArgs, + SpendPaymentArgs, SwapOps, SwapTxFeePolicy, SwapTxTypeWithSecretHash, ToBytes, Transaction, + ValidateNftMakerPaymentArgs}; use common::{block_on, block_on_f01, now_sec}; use crypto::Secp256k1Secret; use ethereum_types::U256; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use mm2_core::mm_ctx::MmArc; use mm2_number::{BigDecimal, BigUint}; -use mm2_test_helpers::for_tests::{erc20_dev_conf, eth_dev_conf, eth_sepolia_conf, nft_dev_conf, sepolia_erc20_dev_conf}; +use mm2_test_helpers::for_tests::{account_balance, disable_coin, enable_erc20_token_v2, enable_eth_with_tokens_v2, + erc20_dev_conf, eth_dev_conf, get_new_address, get_token_info, nft_dev_conf, + MarketMakerIt, Mm2TestConf}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use mm2_test_helpers::for_tests::{eth_sepolia_conf, sepolia_erc20_dev_conf}; +use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalanceMap, EthWithTokensActivationResult, HDAccountAddressId, + TokenInfo}; use serde_json::Value as Json; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] use std::str::FromStr; use std::thread; use std::time::Duration; use web3::contract::{Contract, Options}; use web3::ethabi::Token; -use web3::types::{Address, BlockNumber, TransactionRequest, H256}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] +use web3::types::BlockNumber; +use web3::types::{Address, TransactionRequest, H256}; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const SEPOLIA_MAKER_PRIV: &str = "6e2f3a6223b928a05a3a3622b0c3f3573d03663b704a61a6eb73326de0487928"; +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const SEPOLIA_TAKER_PRIV: &str = "e0be82dca60ff7e4c6d6db339ac9e1ae249af081dba2110bddd281e711608f16"; const NFT_ETH: &str = "NFT_ETH"; const ETH: &str = "ETH"; + +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] const ERC20: &str = "ERC20DEV"; /// # Safety @@ -55,7 +75,9 @@ pub fn maker_swap_v2() -> Address { unsafe { GETH_MAKER_SWAP_V2 } } /// /// GETH_TAKER_SWAP_V2 is set once during initialization before tests start pub fn taker_swap_v2() -> Address { unsafe { GETH_TAKER_SWAP_V2 } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_taker_swap_v2() -> Address { unsafe { SEPOLIA_TAKER_SWAP_V2 } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_maker_swap_v2() -> Address { unsafe { SEPOLIA_MAKER_SWAP_V2 } } /// # Safety /// @@ -69,9 +91,11 @@ pub fn watchers_swap_contract() -> Address { unsafe { GETH_WATCHERS_SWAP_CONTRAC /// /// GETH_ERC20_CONTRACT is set once during initialization before tests start pub fn erc20_contract() -> Address { unsafe { GETH_ERC20_CONTRACT } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_erc20_contract() -> Address { unsafe { SEPOLIA_ERC20_CONTRACT } } /// Return ERC20 dev token contract address in checksum format pub fn erc20_contract_checksum() -> String { checksum_address(&format!("{:02x}", erc20_contract())) } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] pub fn sepolia_erc20_contract_checksum() -> String { checksum_address(&format!("{:02x}", sepolia_erc20_contract())) } /// # Safety /// @@ -81,6 +105,7 @@ pub fn geth_erc721_contract() -> Address { unsafe { GETH_ERC721_CONTRACT } } /// /// GETH_ERC1155_CONTRACT is set once during initialization before tests start pub fn geth_erc1155_contract() -> Address { unsafe { GETH_ERC1155_CONTRACT } } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// # Safety /// /// SEPOLIA_ETOMIC_MAKER_NFT_SWAP_V2 address is set once during initialization before tests start @@ -171,7 +196,7 @@ fn geth_erc712_owner(token_id: U256) -> Address { block_on(erc721_contract.query("ownerOf", Token::Uint(token_id), None, Options::default(), None)).unwrap() } -fn mint_erc1155(to_addr: Address, token_id: U256, amount: U256) { +fn mint_erc1155(to_addr: Address, token_id: U256, amount: u32) { let _guard = GETH_NONCE_LOCK.lock().unwrap(); let erc1155_contract = Contract::from_json(GETH_WEB3.eth(), geth_erc1155_contract(), ERC1155_TEST_ABI.as_bytes()).unwrap(); @@ -181,7 +206,7 @@ fn mint_erc1155(to_addr: Address, token_id: U256, amount: U256) { ( Token::Address(to_addr), Token::Uint(token_id), - Token::Uint(amount), + Token::Uint(U256::from(amount)), Token::Bytes("".into()), ), geth_account(), @@ -200,10 +225,15 @@ fn mint_erc1155(to_addr: Address, token_id: U256, amount: U256) { )) .unwrap(); + // check that "balanceOf" from ERC11155 returns the exact amount of token without any decimals or scaling factors + let balance_dec = balance.to_string().parse::().unwrap(); assert_eq!( - balance, amount, + balance_dec, + BigDecimal::from(amount), "The balance of tokenId {:?} for address {:?} does not match the expected amount {:?}.", - token_id, to_addr, amount + token_id, + to_addr, + amount ); } @@ -381,7 +411,7 @@ fn global_nft_with_random_privkey( if let Some(nft_type) = nft_type { match nft_type { TestNftType::Erc1155 { token_id, amount } => { - mint_erc1155(my_address, U256::from(token_id), U256::from(amount)); + mint_erc1155(my_address, U256::from(token_id), amount); block_on(fill_erc1155_info( &global_nft, geth_erc1155_contract(), @@ -398,6 +428,7 @@ fn global_nft_with_random_privkey( global_nft } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Can be used to generate coin from Sepolia Maker/Taker priv keys. fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { let swap_addr = SwapAddresses { @@ -458,6 +489,7 @@ fn sepolia_coin_from_privkey(ctx: &MmArc, secret: &'static str, ticker: &str, co coin } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] fn get_or_create_sepolia_coin(ctx: &MmArc, priv_key: &'static str, ticker: &str, conf: &Json, erc20: bool) -> EthCoin { match block_on(lp_coinfind(ctx, ticker)).unwrap() { None => sepolia_coin_from_privkey(ctx, priv_key, ticker, conf, erc20), @@ -858,6 +890,7 @@ fn send_and_spend_erc20_maker_payment_priority_fee() { send_and_spend_erc20_maker_payment_impl(SwapTxFeePolicy::Medium); } +#[cfg(any(feature = "sepolia-maker-swap-v2-tests", feature = "sepolia-taker-swap-v2-tests"))] /// Wait for all pending transactions for the given address to be confirmed fn wait_pending_transactions(wallet_address: Address) { let _guard = SEPOLIA_NONCE_LOCK.lock().unwrap(); @@ -1195,9 +1228,8 @@ pub struct TestNftSwapInfo { pub token_id: Vec, /// The type of smart contract that governs this NFT pub contract_type: Coin::ContractType, - /// Etomic swap contract address - pub swap_contract_address: Coin::ContractAddress, } + struct NftActivationV2Args { swap_contract_address: Address, fallback_swap_contract_address: Address, @@ -1265,7 +1297,6 @@ fn setup_test( token_address: token_contract, token_id, contract_type, - swap_contract_address: activation.swap_v2_contracts.nft_maker_swap_v2_contract, }; NftTestSetup { @@ -1285,7 +1316,6 @@ fn send_nft_maker_payment(setup: &NftTestSetup, amount: BigDecimal) -> SignedEth token_address: &setup.nft_swap_info.token_address, token_id: &setup.nft_swap_info.token_id, contract_type: &setup.nft_swap_info.contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; let send_payment_args = SendNftMakerPaymentArgs:: { time_lock: setup.time_lock, @@ -1315,7 +1345,6 @@ fn validate_nft_maker_payment(setup: &NftTestSetup, maker_payment: &SignedEthTx, token_address: &setup.nft_swap_info.token_address, token_id: &setup.nft_swap_info.token_id, contract_type: &setup.nft_swap_info.contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; let validate_args = ValidateNftMakerPaymentArgs { maker_payment_tx: maker_payment, @@ -1344,7 +1373,6 @@ fn spend_nft_maker_payment( maker_pub: &setup.maker_global_nft.derive_htlc_pubkey_v2(&[]), swap_unique_data: &[], contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; block_on(setup.taker_global_nft.spend_nft_maker_payment_v2(spend_payment_args)).unwrap() } @@ -1362,7 +1390,6 @@ fn refund_nft_maker_payment( taker_secret: &setup.taker_secret, swap_unique_data: &[], contract_type, - swap_contract_address: &setup.nft_swap_info.swap_contract_address, }; match refund_type { RefundType::Timelock => { @@ -1449,11 +1476,11 @@ fn eth_coin_v2_activation_with_random_privkey( coin } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_eth() { - // sepolia test - thread::sleep(Duration::from_secs(5)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1511,10 +1538,11 @@ fn send_and_refund_taker_funding_by_secret_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_by_secret_erc20() { - thread::sleep(Duration::from_secs(130)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -1573,11 +1601,11 @@ fn send_and_refund_taker_funding_by_secret_erc20() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 200); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { - thread::sleep(Duration::from_secs(12)); - // sepolia test + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1638,10 +1666,11 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_eth() { - // sepolia test + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1745,11 +1774,11 @@ fn taker_send_approve_and_spend_eth() { block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn taker_send_approve_and_spend_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(9)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -1853,11 +1882,11 @@ fn taker_send_approve_and_spend_erc20() { block_on(taker_coin.wait_for_taker_payment_spend(&spend_tx, 0u64, payment_time_lock)).unwrap(); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { - // sepolia test - thread::sleep(Duration::from_secs(25)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); @@ -1937,11 +1966,11 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_eth() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(28)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -2023,11 +2052,11 @@ fn send_and_refund_taker_funding_exceed_payment_timelock_erc20() { wait_for_confirmations(&taker_coin, &funding_tx_refund, 100); } -#[ignore] +#[cfg(feature = "sepolia-taker-swap-v2-tests")] #[test] fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { - // sepolia test - thread::sleep(Duration::from_secs(200)); + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); let maker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); @@ -2081,7 +2110,6 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { trading_amount, }; wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); - thread::sleep(Duration::from_secs(3)); let funding_tx_refund = block_on(taker_coin.refund_taker_funding_timelock(refund_args)).unwrap(); log!( "Taker refunded ERC20 funding after pre-approval lock time was exceeded, tx hash: {:02x}", @@ -2089,3 +2117,622 @@ fn send_and_refund_taker_funding_exceed_pre_approve_timelock_erc20() { ); wait_for_confirmations(&taker_coin, &funding_tx_refund, 150); } + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_timelock_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() - 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: &maker_secret_hash, + taker_secret_hash: &taker_secret_hash, + }; + let refund_args = RefundMakerPaymentTimelockArgs { + payment_tx: &payment_tx.to_bytes(), + time_lock: payment_time_lock, + taker_pub: &taker_pub.to_bytes(), + tx_type_with_secret_hash, + swap_unique_data: &[], + watcher_reward: false, + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); + log!( + "Maker refunded ETH payment after timelock, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_timelock_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() - 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let tx_type_with_secret_hash = SwapTxTypeWithSecretHash::MakerPaymentV2 { + maker_secret_hash: &maker_secret_hash, + taker_secret_hash: &taker_secret_hash, + }; + let refund_args = RefundMakerPaymentTimelockArgs { + payment_tx: &payment_tx.to_bytes(), + time_lock: payment_time_lock, + taker_pub: &taker_pub.to_bytes(), + tx_type_with_secret_hash, + swap_unique_data: &[], + watcher_reward: false, + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); + log!( + "Maker refunded ERC20 payment after timelock, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_secret_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let refund_args = RefundMakerPaymentSecretArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + taker_secret: &taker_secret, + taker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); + log!( + "Maker refunded ETH payment using taker secret, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_maker_payment_and_refund_secret_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let refund_args = RefundMakerPaymentSecretArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + taker_secret: &taker_secret, + taker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx_refund = block_on(maker_coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); + log!( + "Maker refunded ERC20 payment using taker secret, tx hash: {:02x}", + payment_tx_refund.tx_hash() + ); + wait_for_confirmations(&maker_coin, &payment_tx_refund, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_and_spend_maker_payment_eth() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ETH, ð_sepolia_conf(), false); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ETH, ð_sepolia_conf(), false); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_address = block_on(taker_coin.my_addr()); + let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ETH payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let validation_args = ValidateMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + maker_pub, + swap_unique_data: &[], + }; + block_on(taker_coin.validate_maker_payment_v2(validation_args)).unwrap(); + log!("Taker validated maker ETH payment"); + + let spend_args = SpendMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + maker_secret: &maker_secret, + maker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); + log!("Taker spent maker ETH payment, tx hash: {:02x}", spend_tx.tx_hash()); + wait_for_confirmations(&taker_coin, &spend_tx, 100); +} + +#[cfg(feature = "sepolia-maker-swap-v2-tests")] +#[test] +fn send_and_spend_maker_payment_erc20() { + let _guard = SEPOLIA_TESTS_LOCK.lock().unwrap(); + + let erc20_conf = &sepolia_erc20_dev_conf(&sepolia_erc20_contract_checksum()); + let taker_coin = get_or_create_sepolia_coin(&MM_CTX1, SEPOLIA_TAKER_PRIV, ERC20, erc20_conf, true); + let maker_coin = get_or_create_sepolia_coin(&MM_CTX, SEPOLIA_MAKER_PRIV, ERC20, erc20_conf, true); + + let taker_secret = vec![0; 32]; + let taker_secret_hash = sha256(&taker_secret).to_vec(); + let maker_secret = vec![1; 32]; + let maker_secret_hash = sha256(&maker_secret).to_vec(); + let payment_time_lock = now_sec() + 1000; + + let maker_address = block_on(maker_coin.my_addr()); + let taker_address = block_on(taker_coin.my_addr()); + let maker_pub = &maker_coin.derive_htlc_pubkey_v2(&[]); + let taker_pub = &taker_coin.derive_htlc_pubkey_v2(&[]); + + let trading_amount = BigDecimal::from_str("0.0001").unwrap(); + + let payment_args = SendMakerPaymentArgs { + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + taker_pub, + swap_unique_data: &[], + }; + wait_pending_transactions(Address::from_slice(maker_address.as_bytes())); + let payment_tx = block_on(maker_coin.send_maker_payment_v2(payment_args)).unwrap(); + log!("Maker sent ERC20 payment, tx hash: {:02x}", payment_tx.tx_hash()); + wait_for_confirmations(&maker_coin, &payment_tx, 100); + + let validation_args = ValidateMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + amount: trading_amount.clone(), + maker_pub, + swap_unique_data: &[], + }; + block_on(taker_coin.validate_maker_payment_v2(validation_args)).unwrap(); + log!("Taker validated maker ERC20 payment"); + + let spend_args = SpendMakerPaymentArgs { + maker_payment_tx: &payment_tx, + time_lock: payment_time_lock, + taker_secret_hash: &taker_secret_hash, + maker_secret_hash: &maker_secret_hash, + maker_secret: &maker_secret, + maker_pub, + swap_unique_data: &[], + amount: trading_amount, + }; + wait_pending_transactions(Address::from_slice(taker_address.as_bytes())); + let spend_tx = block_on(taker_coin.spend_maker_payment_v2(spend_args)).unwrap(); + log!("Taker spent maker ERC20 payment, tx hash: {:02x}", spend_tx.tx_hash()); + wait_for_confirmations(&taker_coin, &spend_tx, 100); +} + +#[test] +fn test_eth_erc20_hd() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([eth_dev_conf(), erc20_dev_conf(&erc20_contract_checksum())]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + // Withdraw from HD account 0, change address 0, index 0 + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[0].address, + "0x1737F1FaB40c6Fd3dc729B51C0F97DB3297CCA93" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 0, change address 0, index 1 + let path_to_address = HDAccountAddressId { + account_id: 0, + chain: Bip44Chain::External, + address_id: 1, + }; + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[1].address, + "0xDe841899aB4A22E23dB21634e54920aDec402397" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + let get_new_address = block_on(get_new_address(&mm_hd, "ETH", 0, Some(Bip44Chain::External))); + assert!(get_new_address.new_address.balance.contains_key("ETH")); + // Make sure balance is returned for any token enabled with ETH as platform coin + assert!(get_new_address.new_address.balance.contains_key("ERC20DEV")); + assert_eq!( + get_new_address.new_address.address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + // Make sure that the address is also added to tokens + let account_balance = block_on(account_balance(&mm_hd, "ERC20DEV", 0, Bip44Chain::External)); + assert_eq!( + account_balance.addresses[2].address, + "0x4249E165a68E4FF9C41B1C3C3b4245c30ecB43CC" + ); + + block_on(mm_hd.stop()).unwrap(); + + // Enable HD account 77, change address 0, index 7 + let path_to_address = HDAccountAddressId { + account_id: 77, + chain: Bip44Chain::External, + address_id: 7, + }; + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + let eth_enable = block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &["ERC20DEV"], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address), + )); + let activation_result = match eth_enable { + EthWithTokensActivationResult::HD(hd) => hd, + _ => panic!("Expected EthWithTokensActivationResult::HD"), + }; + let balance = match activation_result.wallet_balance { + EnableCoinBalanceMap::HD(hd) => hd, + _ => panic!("Expected EnableCoinBalance::HD"), + }; + let account = balance.accounts.get(0).expect("Expected account at index 0"); + assert_eq!( + account.addresses[7].address, + "0xa420a4DBd8C50e6240014Db4587d2ec8D0cE0e6B" + ); + assert_eq!(account.addresses[0].balance.len(), 2); + assert!(account.addresses[0].balance.contains_key("ETH")); + assert!(account.addresses[0].balance.contains_key("ERC20DEV")); + + block_on(mm_hd.stop()).unwrap(); +} + +#[test] +fn test_enable_custom_erc20() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let coins = json!([eth_dev_conf()]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + // Enable platform coin in HD mode + block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &[], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address.clone()), + )); + + // Test `get_token_info` rpc, we also use it to get the token symbol to use it as the ticker + let protocol = erc20_dev_conf(&erc20_contract_checksum())["protocol"].clone(); + let TokenInfo::ERC20(custom_token_info) = block_on(get_token_info(&mm_hd, protocol.clone())).info; + let ticker = custom_token_info.symbol; + assert_eq!(ticker, "QTC"); + assert_eq!(custom_token_info.decimals, 8); + + // Enable the custom token in HD mode + block_on(enable_erc20_token_v2( + &mm_hd, + &ticker, + Some(protocol.clone()), + 60, + Some(path_to_address.clone()), + )) + .unwrap(); + + // Test that the custom token is wallet only by using it in a swap + let buy = block_on(mm_hd.rpc(&json!({ + "userpass": mm_hd.userpass, + "method": "buy", + "base": "ETH", + "rel": ticker, + "price": "1", + "volume": "1", + }))) + .unwrap(); + assert!(!buy.0.is_success(), "buy success, but should fail: {}", buy.1); + assert!( + buy.1.contains(&format!("Rel coin {} is wallet only", ticker)), + "Expected error message indicating that the token is wallet only, but got: {}", + buy.1 + ); + + // Enabling the same custom token using a different ticker should fail + let err = block_on(enable_erc20_token_v2( + &mm_hd, + "ERC20DEV", + Some(protocol.clone()), + 60, + Some(path_to_address), + )) + .unwrap_err(); + let expected_error_type = "CustomTokenError"; + assert_eq!(err["error_type"], expected_error_type); + let expected_error_data = json!({ + "TokenWithSameContractAlreadyActivated": { + "ticker": ticker, + "contract_address": protocol["protocol_data"]["contract_address"] + } + }); + assert_eq!(err["error_data"], expected_error_data); + + // Disable the custom token + block_on(disable_coin(&mm_hd, &ticker, true)); +} + +#[test] +fn test_enable_custom_erc20_with_duplicate_contract_in_config() { + const PASSPHRASE: &str = "tank abandon bind salon remove wisdom net size aspect direct source fossil"; + + let erc20_dev_conf = erc20_dev_conf(&erc20_contract_checksum()); + let coins = json!([eth_dev_conf(), erc20_dev_conf]); + let swap_contract = format!("0x{}", hex::encode(swap_contract())); + + let path_to_address = HDAccountAddressId::default(); + let conf = Mm2TestConf::seednode_with_hd_account(PASSPHRASE, &coins); + let mm_hd = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + let (_mm_dump_log, _mm_dump_dashboard) = mm_hd.mm_dump(); + log!("Alice log path: {}", mm_hd.log_path.display()); + + // Enable platform coin in HD mode + block_on(enable_eth_with_tokens_v2( + &mm_hd, + "ETH", + &[], + &swap_contract, + &[GETH_RPC_URL], + 60, + Some(path_to_address.clone()), + )); + + let protocol = erc20_dev_conf["protocol"].clone(); + // Enable the custom token in HD mode. + // Since the contract is already in the coins config, this should fail with an error + // that specifies the ticker in config so that the user can enable the right coin. + let err = block_on(enable_erc20_token_v2( + &mm_hd, + "QTC", + Some(protocol.clone()), + 60, + Some(path_to_address.clone()), + )) + .unwrap_err(); + let expected_error_type = "CustomTokenError"; + assert_eq!(err["error_type"], expected_error_type); + let expected_error_data = json!({ + "DuplicateContractInConfig": { + "ticker_in_config": "ERC20DEV" + } + }); + assert_eq!(err["error_data"], expected_error_data); + + // Another way is to use the `get_token_info` RPC and use the config ticker to enable the token. + let custom_token_info = block_on(get_token_info(&mm_hd, protocol)); + assert!(custom_token_info.config_ticker.is_some()); + let config_ticker = custom_token_info.config_ticker.unwrap(); + assert_eq!(config_ticker, "ERC20DEV"); + // Parameters passed here are for normal enabling of a coin in config and not for a custom token + block_on(enable_erc20_token_v2( + &mm_hd, + &config_ticker, + None, + 60, + Some(path_to_address), + )) + .unwrap(); + + // Disable the custom token, this to check that it was enabled correctly + block_on(disable_coin(&mm_hd, &config_ticker, true)); +} diff --git a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs index bcb125ba26..2897ff0402 100644 --- a/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/qrc20_tests.rs @@ -793,7 +793,7 @@ fn test_wait_for_tx_spend() { // first try to check if the wait_for_htlc_tx_spend() returns an error correctly let wait_until = wait_until_sec(5); - let tx_err = block_on_f01(maker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + let tx_err = block_on(maker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &payment_tx_hex, secret_hash: &[], wait_until, @@ -830,7 +830,7 @@ fn test_wait_for_tx_spend() { }); let wait_until = wait_until_sec(120); - let found = block_on_f01(maker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { + let found = block_on(maker_coin.wait_for_htlc_tx_spend(WaitForHTLCTxSpendArgs { tx_bytes: &payment_tx_hex, secret_hash: &[], wait_until, diff --git a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs index 4c028b28b6..b9684e1649 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_proto_v2_tests.rs @@ -551,8 +551,8 @@ fn send_and_refund_maker_payment_timelock() { maker_secret_hash, }, swap_unique_data: &[], - swap_contract_address: &None, watcher_reward: false, + amount: Default::default(), }; let refund_tx = block_on(coin.refund_maker_payment_v2_timelock(refund_args)).unwrap(); @@ -614,6 +614,7 @@ fn send_and_refund_maker_payment_taker_secret() { swap_unique_data: &[], taker_secret, taker_pub, + amount: Default::default(), }; let refund_tx = block_on(coin.refund_maker_payment_v2_secret(refund_args)).unwrap(); diff --git a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs index 102e8eacf5..b9dd9d9d35 100644 --- a/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/swap_watcher_tests.rs @@ -94,6 +94,7 @@ fn start_swaps_and_get_balances( alice_privkey: &str, bob_privkey: &str, watcher_privkey: &str, + custom_locktime: Option, ) -> BalanceResult { let coins = json!([ eth_dev_conf(), @@ -102,7 +103,10 @@ fn start_swaps_and_get_balances( mycoin1_conf(1000) ]); - let alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + let mut alice_conf = Mm2TestConf::seednode(&format!("0x{}", alice_privkey), &coins); + if let Some(locktime) = custom_locktime { + alice_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_alice = block_on(MarketMakerIt::start_with_envs( alice_conf.conf.clone(), alice_conf.rpc_password.clone(), @@ -113,7 +117,10 @@ fn start_swaps_and_get_balances( let (_alice_dump_log, _alice_dump_dashboard) = mm_alice.mm_dump(); log!("Alice log path: {}", mm_alice.log_path.display()); - let bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + let mut bob_conf = Mm2TestConf::light_node(&format!("0x{}", bob_privkey), &coins, &[&mm_alice.ip.to_string()]); + if let Some(locktime) = custom_locktime { + bob_conf.conf["payment_locktime"] = locktime.into(); + } let mut mm_bob = block_on(MarketMakerIt::start_with_envs( bob_conf.conf.clone(), bob_conf.rpc_password, @@ -159,13 +166,16 @@ fn start_swaps_and_get_balances( ), }; - let watcher_conf = Mm2TestConf::watcher_light_node( + let mut watcher_conf = Mm2TestConf::watcher_light_node( &format!("0x{}", watcher_privkey), &coins, &[&mm_alice.ip.to_string()], watcher_conf, ) .conf; + if let Some(locktime) = custom_locktime { + watcher_conf["payment_locktime"] = locktime.into(); + } let mut mm_watcher = block_on(MarketMakerIt::start_with_envs( watcher_conf, @@ -272,9 +282,17 @@ fn check_actual_events(mm_alice: &MarketMakerIt, uuid: &str, expected_events: &[ status_response } -fn run_taker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> (MarketMakerIt, Mm2TestConf) { +fn run_taker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> (MarketMakerIt, Mm2TestConf) { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + let mut conf = Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes); + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password.clone(), @@ -311,13 +329,21 @@ fn restart_taker_and_wait_until(conf: &Mm2TestConf, envs: &[(&str, &str)], wait_ mm_alice } -fn run_maker_node(coins: &Value, envs: &[(&str, &str)], seednodes: &[&str]) -> MarketMakerIt { +fn run_maker_node( + coins: &Value, + envs: &[(&str, &str)], + seednodes: &[&str], + custom_locktime: Option, +) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = if seednodes.is_empty() { + let mut conf = if seednodes.is_empty() { Mm2TestConf::seednode(&format!("0x{}", privkey), coins) } else { Mm2TestConf::light_node(&format!("0x{}", privkey), coins, seednodes) }; + if let Some(locktime) = custom_locktime { + conf.conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf.conf.clone(), conf.rpc_password, @@ -341,9 +367,13 @@ fn run_watcher_node( envs: &[(&str, &str)], seednodes: &[&str], watcher_conf: WatcherConf, + custom_locktime: Option, ) -> MarketMakerIt { let privkey = hex::encode(random_secp256k1_secret()); - let conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + let mut conf = Mm2TestConf::watcher_light_node(&format!("0x{}", privkey), coins, seednodes, watcher_conf).conf; + if let Some(locktime) = custom_locktime { + conf["payment_locktime"] = locktime.into(); + } let mm = block_on(MarketMakerIt::start_with_envs( conf, DEFAULT_RPC_PASSWORD.to_string(), @@ -365,11 +395,13 @@ fn run_watcher_node( #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[ - &mm_bob.ip.to_string(), - ]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -377,7 +409,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -424,11 +456,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_wait_for_taker #[test] fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = - run_taker_node(&coins, &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], &[&mm_bob - .ip - .to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node( + &coins, + &[("TAKER_FAIL_AT", "maker_payment_spend_panic")], + &[&mm_bob.ip.to_string()], + None, + ); let watcher_conf = WatcherConf { wait_taker_payment: 0., @@ -436,7 +470,7 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ refund_start_factor: 1.5, search_interval: 1.0, }; - let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_bob.ip.to_string()], watcher_conf, None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -479,15 +513,13 @@ fn test_taker_saves_the_swap_as_successful_after_restart_panic_at_maker_payment_ #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_wait_for_taker_payment_spend() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic"), - ], + &[("TAKER_FAIL_AT", "wait_for_taker_payment_spend_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -496,12 +528,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -523,11 +550,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -552,15 +575,13 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_panic_at_taker_payment_refund() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mm_seednode = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[]); - let mut mm_bob = run_maker_node(&coins, &[("USE_TEST_LOCKTIME", "")], &[&mm_seednode.ip.to_string()]); + let mm_seednode = run_maker_node(&coins, &[], &[], Some(60)); + let mut mm_bob = run_maker_node(&coins, &[], &[&mm_seednode.ip.to_string()], Some(60)); let (mut mm_alice, mut alice_conf) = run_taker_node( &coins, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TAKER_FAIL_AT", "taker_payment_refund_panic"), - ], + &[("TAKER_FAIL_AT", "taker_payment_refund_panic")], &[&mm_seednode.ip.to_string()], + Some(60), ); let watcher_conf = WatcherConf { @@ -569,12 +590,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa refund_start_factor: 0., search_interval: 1., }; - let mut mm_watcher = run_watcher_node( - &coins, - &[("USE_TEST_LOCKTIME", "")], - &[&mm_seednode.ip.to_string()], - watcher_conf, - ); + let mut mm_watcher = run_watcher_node(&coins, &[], &[&mm_seednode.ip.to_string()], watcher_conf, Some(60)); let uuids = block_on(start_swaps( &mut mm_bob, @@ -596,11 +612,7 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa block_on(mm_alice.stop()).unwrap(); - let mm_alice = restart_taker_and_wait_until( - &alice_conf, - &[("USE_TEST_LOCKTIME", "")], - &format!("[swap uuid={}] Finished", &uuids[0]), - ); + let mm_alice = restart_taker_and_wait_until(&alice_conf, &[], &format!("[swap uuid={}] Finished", &uuids[0])); let expected_events = [ "Started", @@ -628,8 +640,8 @@ fn test_taker_saves_the_swap_as_finished_after_restart_taker_payment_refunded_pa #[test] fn test_taker_completes_swap_after_restart() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -673,8 +685,8 @@ fn test_taker_completes_swap_after_restart() { #[test] fn test_taker_completes_swap_after_taker_payment_spent_while_offline() { let coins = json!([mycoin_conf(1000), mycoin1_conf(1000)]); - let mut mm_bob = run_maker_node(&coins, &[], &[]); - let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()]); + let mut mm_bob = run_maker_node(&coins, &[], &[], None); + let (mut mm_alice, mut alice_conf) = run_taker_node(&coins, &[], &[&mm_bob.ip.to_string()], None); let uuids = block_on(start_swaps( &mut mm_bob, @@ -738,6 +750,7 @@ fn test_watcher_spends_maker_payment_utxo_utxo() { &alice_privkey, &bob_privkey, &watcher_privkey, + None, ); let acoin_volume = BigDecimal::from_str("50").unwrap(); @@ -778,6 +791,7 @@ fn test_watcher_spends_maker_payment_utxo_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -807,6 +821,7 @@ fn test_watcher_spends_maker_payment_eth_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -851,6 +866,7 @@ fn test_watcher_spends_maker_payment_eth_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let eth_volume = BigDecimal::from_str("0.01").unwrap(); @@ -884,6 +900,7 @@ fn test_watcher_spends_maker_payment_erc20_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let jst_volume = BigDecimal::from_str("1").unwrap(); @@ -914,6 +931,7 @@ fn test_watcher_spends_maker_payment_utxo_erc20() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -947,6 +965,7 @@ fn test_watcher_spends_maker_payment_erc20_utxo() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); let mycoin_volume = BigDecimal::from_str("1").unwrap(); @@ -993,11 +1012,12 @@ fn test_watcher_refunds_taker_payment_utxo() { 25., 25., 2., - &[("USE_TEST_LOCKTIME", "")], + &[], SwapFlow::WatcherRefundsTakerPayment, alice_privkey, bob_privkey, watcher_privkey, + Some(60), ); assert_eq!( @@ -1019,11 +1039,12 @@ fn test_watcher_refunds_taker_payment_eth() { 0.01, 0.01, 1., - &[("USE_TEST_LOCKTIME", ""), ("USE_WATCHER_REWARD", "")], + &[("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); assert_eq!(balances.alice_bcoin_balance_after, balances.alice_bcoin_balance_before); @@ -1042,15 +1063,12 @@ fn test_watcher_refunds_taker_payment_erc20() { 100., 100., 0.01, - &[ - ("USE_TEST_LOCKTIME", ""), - ("TEST_COIN_PRICE", "0.01"), - ("USE_WATCHER_REWARD", ""), - ], + &[("TEST_COIN_PRICE", "0.01"), ("USE_WATCHER_REWARD", "")], SwapFlow::WatcherRefundsTakerPayment, &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + Some(60), ); let erc20_volume = BigDecimal::from_str("1").unwrap(); @@ -1082,6 +1100,7 @@ fn test_watcher_waits_for_taker_utxo() { alice_privkey, bob_privkey, watcher_privkey, + None, ); } @@ -1102,6 +1121,7 @@ fn test_watcher_waits_for_taker_eth() { &alice_coin.display_priv_key().unwrap()[2..], &bob_coin.display_priv_key().unwrap()[2..], &watcher_coin.display_priv_key().unwrap()[2..], + None, ); } @@ -3283,5 +3303,5 @@ fn test_watcher_reward() { let watcher_reward = block_on(utxo_coin.get_maker_watcher_reward(&MmCoinEnum::UtxoCoin(utxo_coin.clone()), None, timeout)).unwrap(); - assert!(matches!(watcher_reward, None)); + assert!(watcher_reward.is_none()); } diff --git a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs index 043223357e..c602c93662 100644 --- a/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs +++ b/mm2src/mm2_main/tests/docker_tests/tendermint_tests.rs @@ -5,7 +5,7 @@ use mm2_test_helpers::for_tests::{atom_testnet_conf, disable_coin, disable_coin_ enable_tendermint_token, enable_tendermint_without_balance, get_tendermint_my_tx_history, ibc_withdraw, iris_ibc_nucleus_testnet_conf, my_balance, nucleus_testnet_conf, orderbook, orderbook_v2, send_raw_transaction, - set_price, withdraw_v1, MarketMakerIt, Mm2TestConf}; + set_price, tendermint_validators, withdraw_v1, MarketMakerIt, Mm2TestConf}; use mm2_test_helpers::structs::{Bip44Chain, HDAccountAddressId, OrderbookAddress, OrderbookV2Response, RpcV2Response, TendermintActivationResult, TransactionDetails}; use serde_json::json; @@ -314,7 +314,7 @@ fn test_custom_gas_limit_on_tendermint_withdraw() { #[test] fn test_tendermint_ibc_withdraw() { // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-2"; + const IBC_SOURCE_CHANNEL: &str = "channel-3"; const IBC_TARGET_ADDRESS: &str = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"; const MY_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; @@ -360,7 +360,7 @@ fn test_tendermint_ibc_withdraw() { #[test] fn test_tendermint_ibc_withdraw_hd() { // visit `{swagger_address}/ibc/core/channel/v1/channels?pagination.limit=10000` to see the full list of ibc channels - const IBC_SOURCE_CHANNEL: &str = "channel-2"; + const IBC_SOURCE_CHANNEL: &str = "channel-3"; const IBC_TARGET_ADDRESS: &str = "nuc150evuj4j7k9kgu38e453jdv9m3u0ft2n4fgzfr"; const MY_ADDRESS: &str = "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy"; @@ -489,7 +489,7 @@ fn test_tendermint_tx_history() { const TEST_SEED: &str = "Vdo8Xt8pTAetRlMq3kV0LzE393eVYbPSn5Mhtw4p"; const TX_FINISHED_LOG: &str = "Tx history fetching finished for NUCLEUS-TEST."; const TX_HISTORY_PAGE_LIMIT: usize = 50; - const NUCLEUS_EXPECTED_TX_COUNT: u64 = 7; + const NUCLEUS_EXPECTED_TX_COUNT: u64 = 9; const IRIS_IBC_EXPECTED_TX_COUNT: u64 = 1; let nucleus_constant_history_txs = include_str!("../../../mm2_test_helpers/dummy_files/nucleus-history.json"); @@ -651,6 +651,32 @@ fn test_passive_coin_and_force_disable() { block_on(disable_coin_err(&mm, token, false)); } +#[test] +fn test_tendermint_validators_rpc() { + let coins = json!([nucleus_testnet_conf()]); + let platform_coin = coins[0]["coin"].as_str().unwrap(); + + let conf = Mm2TestConf::seednode(TENDERMINT_TEST_SEED, &coins); + let mm = MarketMakerIt::start(conf.conf, conf.rpc_password, None).unwrap(); + + let activation_res = block_on(enable_tendermint( + &mm, + platform_coin, + &[], + NUCLEUS_TESTNET_RPC_URLS, + false, + )); + assert!(&activation_res.get("result").unwrap().get("address").is_some()); + + let validators_raw_response = block_on(tendermint_validators(&mm, platform_coin, "All", 10, 1)); + + assert_eq!( + validators_raw_response["result"]["validators"][0]["operator_address"], + "nucvaloper15d4sf4z6y0vk9dnum8yzkvr9c3wq4q897vefpu" + ); + assert_eq!(validators_raw_response["result"]["validators"][0]["jailed"], false); +} + mod swap { use super::*; diff --git a/mm2src/mm2_number/src/lib.rs b/mm2src/mm2_number/src/lib.rs index 6d1d20f1ca..ba8b520ec0 100644 --- a/mm2src/mm2_number/src/lib.rs +++ b/mm2src/mm2_number/src/lib.rs @@ -13,7 +13,7 @@ pub use num_bigint; pub use num_rational; pub use bigdecimal::BigDecimal; -pub use num_bigint::{BigInt, BigUint}; +pub use num_bigint::{BigInt, BigUint, ParseBigIntError}; pub use num_rational::BigRational; pub use paste::paste; diff --git a/mm2src/mm2_p2p/Cargo.toml b/mm2src/mm2_p2p/Cargo.toml index 6b7f43e7f4..cbbe711783 100644 --- a/mm2src/mm2_p2p/Cargo.toml +++ b/mm2src/mm2_p2p/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [features] -default = [] +default = ["application"] application = ["dep:mm2_number"] [lib] @@ -38,14 +38,17 @@ void = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures-rustls = "0.24" instant = "0.1.12" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["dns", "identify", "floodsub", "gossipsub", "noise", "ping", "request-response", "secp256k1", "tcp", "tokio", "websocket", "macros", "yamux"] } +timed-map = { version = "1.1.1", features = ["rustc-hash"] } tokio = { version = "1.20", default-features = false } [target.'cfg(target_arch = "wasm32")'.dependencies] futures-rustls = "0.22" instant = { version = "0.1.12", features = ["wasm-bindgen"] } -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify", "floodsub", "noise", "gossipsub", "ping", "request-response", "secp256k1", "wasm-ext", "wasm-ext-websocket", "macros", "yamux"] } +timed-map = { version = "1.1.1", features = ["rustc-hash", "wasm"] } [dev-dependencies] async-std = "1.6.2" env_logger = "0.9.3" +common = { path = "../common", features = ["for-tests"] } diff --git a/mm2src/mm2_p2p/src/application/request_response/network_info.rs b/mm2src/mm2_p2p/src/application/request_response/network_info.rs index c8dece2ef5..4d610d932c 100644 --- a/mm2src/mm2_p2p/src/application/request_response/network_info.rs +++ b/mm2src/mm2_p2p/src/application/request_response/network_info.rs @@ -6,4 +6,6 @@ use serde::{Deserialize, Serialize}; pub enum NetworkInfoRequest { /// Get MM2 version of nodes added to stats collection GetMm2Version, + /// Get UTC timestamp in seconds from the target peer + GetPeerUtcTimestamp, } diff --git a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs index 9d58da4e1e..6d3ccb9d69 100644 --- a/mm2src/mm2_p2p/src/behaviours/atomicdex.rs +++ b/mm2src/mm2_p2p/src/behaviours/atomicdex.rs @@ -7,6 +7,7 @@ use futures::{channel::oneshot, use futures_rustls::rustls; use futures_ticker::Ticker; use instant::Duration; +use lazy_static::lazy_static; use libp2p::core::transport::Boxed as BoxedTransport; use libp2p::core::{ConnectedPoint, Endpoint}; use libp2p::floodsub::{Floodsub, FloodsubEvent, Topic as FloodsubTopic}; @@ -23,16 +24,20 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::iter; use std::net::IpAddr; +use std::sync::{Mutex, MutexGuard}; use std::task::{Context, Poll}; +use timed_map::{MapKind, StdClock, TimedMap}; use super::peers_exchange::{PeerAddresses, PeersExchange, PeersExchangeRequest, PeersExchangeResponse}; use super::ping::AdexPing; use super::request_response::{build_request_response_behaviour, PeerRequest, PeerResponse, RequestResponseBehaviour, RequestResponseSender}; +use crate::application::request_response::network_info::NetworkInfoRequest; +use crate::application::request_response::P2PRequest; use crate::network::{get_all_network_seednodes, DEFAULT_NETID}; use crate::relay_address::{RelayAddress, RelayAddressError}; use crate::swarm_runtime::SwarmRuntime; -use crate::{NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; +use crate::{decode_message, encode_message, NetworkInfo, NetworkPorts, RequestResponseBehaviourEvent}; pub use libp2p::gossipsub::{Behaviour as Gossipsub, IdentTopic, MessageAuthenticity, MessageId, Topic, TopicHash}; pub use libp2p::gossipsub::{ConfigBuilder as GossipsubConfigBuilder, Event as GossipsubEvent, @@ -50,6 +55,21 @@ const ANNOUNCE_INTERVAL: Duration = Duration::from_secs(600); const ANNOUNCE_INITIAL_DELAY: Duration = Duration::from_secs(60); const CHANNEL_BUF_SIZE: usize = 1024 * 8; +/// Used in time validation logic for each peer which runs immediately after the +/// `ConnectionEstablished` event. +/// +/// Be careful when updating this value, we have some defaults (like for swaps) +/// depending on this. +pub const MAX_TIME_GAP_FOR_CONNECTED_PEER: u64 = 20; + +/// Used for storing peers in [`RECENTLY_DIALED_PEERS`]. +const DIAL_RETRY_DELAY: Duration = Duration::from_secs(60 * 5); + +lazy_static! { + /// Tracks recently dialed peers to avoid repeated connection attempts. + static ref RECENTLY_DIALED_PEERS: Mutex> = Mutex::new(TimedMap::new_with_map_kind(MapKind::FxHashMap)); +} + pub const DEPRECATED_NETID_LIST: &[u16] = &[ 7777, // TODO: keep it inaccessible until Q2 of 2024. ]; @@ -162,6 +182,24 @@ pub enum AdexBehaviourCmd { }, } +/// Determines if a dial attempt to the remote should be made. +/// +/// Returns `false` if a dial attempt to the given address has already been made, +/// in which case the caller must skip the dial attempt. +fn check_and_mark_dialed( + recently_dialed_peers: &mut MutexGuard>, + addr: &Multiaddr, +) -> bool { + if recently_dialed_peers.get(addr).is_some() { + info!("Connection attempt was already made recently to '{addr}'."); + return false; + } + + recently_dialed_peers.insert_expirable_unchecked(addr.clone(), (), DIAL_RETRY_DELAY); + + true +} + /// Returns info about directly connected peers. pub async fn get_directly_connected_peers(mut cmd_tx: AdexCmdTx) -> HashMap> { let (result_tx, rx) = oneshot::channel(); @@ -199,6 +237,44 @@ pub async fn get_relay_mesh(mut cmd_tx: AdexCmdTx) -> Vec { rx.await.expect("Tx should be present") } +async fn validate_peer_time(peer: PeerId, mut response_tx: Sender, rp_sender: RequestResponseSender) { + let request = P2PRequest::NetworkInfo(NetworkInfoRequest::GetPeerUtcTimestamp); + let encoded_request = encode_message(&request) + .expect("Static type `PeerInfoRequest::GetPeerUtcTimestamp` should never fail in serialization."); + + match request_one_peer(peer, encoded_request, rp_sender).await { + PeerResponse::Ok { res } => { + if let Ok(timestamp) = decode_message::(&res) { + let now = common::get_utc_timestamp(); + let now: u64 = now + .try_into() + .unwrap_or_else(|_| panic!("`common::get_utc_timestamp` returned invalid data: {}", now)); + + let diff = now.abs_diff(timestamp); + + // If time diff is in the acceptable gap, end the validation here. + if diff <= MAX_TIME_GAP_FOR_CONNECTED_PEER { + debug!( + "Peer '{peer}' is within the acceptable time gap ({MAX_TIME_GAP_FOR_CONNECTED_PEER} seconds); time difference is {diff} seconds." + ); + return; + } + }; + }, + other => { + error!("Unexpected response `{other:?}` from peer `{peer}`"); + // TODO: Ideally, we should send `peer` to end the connection, + // but we don't want to cause a breaking change yet. + return; + }, + } + + // If the function reaches this point, this means validation has failed. + // Send the peer ID to disconnect from it. + error!("Failed to validate the time for peer `{peer}`; disconnecting."); + response_tx.send(peer).await.unwrap(); +} + async fn request_one_peer(peer: PeerId, req: Vec, mut request_response_tx: RequestResponseSender) -> PeerResponse { // Use the internal receiver to receive a response to this request. let (internal_response_tx, internal_response_rx) = oneshot::channel(); @@ -711,12 +787,18 @@ fn start_gossipsub( _ => (), } + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); for relay in bootstrap.choose_multiple(&mut rng, mesh_n) { + if !check_and_mark_dialed(&mut recently_dialed_peers, relay) { + continue; + } + match libp2p::Swarm::dial(&mut swarm, relay.clone()) { Ok(_) => info!("Dialed {}", relay), Err(e) => error!("Dial {:?} failed: {:?}", relay, e), } } + drop(recently_dialed_peers); let mut check_connected_relays_interval = Ticker::new_with_next(CONNECTED_RELAYS_CHECK_INTERVAL, CONNECTED_RELAYS_CHECK_INTERVAL); @@ -724,6 +806,7 @@ fn start_gossipsub( let mut announce_interval = Ticker::new_with_next(ANNOUNCE_INTERVAL, ANNOUNCE_INITIAL_DELAY); let mut listening = false; + let (timestamp_tx, mut timestamp_rx) = futures::channel::mpsc::channel(mesh_n_high); let polling_fut = poll_fn(move |cx: &mut Context| { loop { match swarm.behaviour_mut().cmd_rx.poll_next_unpin(cx) { @@ -733,11 +816,27 @@ fn start_gossipsub( } } + while let Poll::Ready(Some(peer_id)) = timestamp_rx.poll_next_unpin(cx) { + if swarm.disconnect_peer_id(peer_id).is_err() { + error!("Disconnection from `{peer_id}` failed unexpectedly, which should never happen."); + } + } + loop { match swarm.poll_next_unpin(cx) { Poll::Ready(Some(event)) => { debug!("Swarm event {:?}", event); + if let SwarmEvent::ConnectionEstablished { peer_id, .. } = &event { + info!("Validating time data for peer `{peer_id}`."); + let future = validate_peer_time( + *peer_id, + timestamp_tx.clone(), + swarm.behaviour().core.request_response.sender(), + ); + swarm.behaviour().spawn(future); + } + if let SwarmEvent::Behaviour(event) = event { if swarm.behaviour_mut().netid != DEFAULT_NETID { if let AdexBehaviourEvent::Floodsub(FloodsubEvent::Message(message)) = &event { @@ -798,19 +897,29 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses let mut rng = rand::thread_rng(); if connected_relays.len() < mesh_n_low { + let mut recently_dialed_peers = RECENTLY_DIALED_PEERS.lock().unwrap(); let to_connect_num = mesh_n - connected_relays.len(); - let to_connect = swarm - .behaviour_mut() - .core - .peers_exchange - .get_random_peers(to_connect_num, |peer| !connected_relays.contains(peer)); + let to_connect = + swarm + .behaviour_mut() + .core + .peers_exchange + .get_random_peers(to_connect_num, |peer, addresses| { + !connected_relays.contains(peer) + && addresses + .iter() + .any(|addr| check_and_mark_dialed(&mut recently_dialed_peers, addr)) + }); // choose some random bootstrap addresses to connect if peers exchange returned not enough peers if to_connect.len() < to_connect_num { let connect_bootstrap_num = to_connect_num - to_connect.len(); for addr in bootstrap_addresses .iter() - .filter(|addr| !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr)) + .filter(|addr| { + !swarm.behaviour().core.gossipsub.is_connected_to_addr(addr) + && check_and_mark_dialed(&mut recently_dialed_peers, addr) + }) .collect::>() .choose_multiple(&mut rng, connect_bootstrap_num) { @@ -824,11 +933,13 @@ fn maintain_connection_to_relays(swarm: &mut AtomicDexSwarm, bootstrap_addresses if swarm.behaviour().core.gossipsub.is_connected_to_addr(&addr) { continue; } + if let Err(e) = libp2p::Swarm::dial(swarm, addr.clone()) { error!("Peer {} address {} dial error {}", peer, addr, e); } } } + drop(recently_dialed_peers); } if connected_relays.len() > max_n { diff --git a/mm2src/mm2_p2p/src/behaviours/mod.rs b/mm2src/mm2_p2p/src/behaviours/mod.rs index cdfda38c8d..36436efe4e 100644 --- a/mm2src/mm2_p2p/src/behaviours/mod.rs +++ b/mm2src/mm2_p2p/src/behaviours/mod.rs @@ -106,17 +106,16 @@ mod tests { let node1_port = next_port(); let node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::Ok { response: b"test response".to_vec(), @@ -157,19 +156,17 @@ mod tests { impl RequestHandler { fn handle(&mut self, mut cmd_tx: mpsc::Sender, event: AdexBehaviourEvent) { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; self.requests += 1; - assert_eq!(request, b"test request"); - // the first time we should respond the none if self.requests == 1 { let res = AdexResponse::None; @@ -249,17 +246,16 @@ mod tests { let node1_port = next_port(); let _node1 = Node::spawn(node1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; request_received_cpy.store(true, Ordering::Relaxed); - assert_eq!(request, b"test request"); let res = AdexResponse::None; cmd_tx @@ -293,17 +289,15 @@ mod tests { let receiver1_port = next_port(); let receiver1 = Node::spawn(receiver1_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::None; cmd_tx .try_send(AdexBehaviourCmd::SendResponse { res, response_channel }) @@ -313,17 +307,15 @@ mod tests { let receiver2_port = next_port(); let receiver2 = Node::spawn(receiver2_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Err { error: "test error".into(), }; @@ -335,17 +327,15 @@ mod tests { let receiver3_port = next_port(); let receiver3 = Node::spawn(receiver3_port, vec![], move |mut cmd_tx, event| { - let (request, response_channel) = match event { + let response_channel = match event { AdexBehaviourEvent::RequestResponse(RequestResponseBehaviourEvent::InboundRequest { request, response_channel, .. - }) => (request.req, AdexResponseChannel(response_channel)), + }) if request.req == b"test request" => AdexResponseChannel(response_channel), _ => return, }; - assert_eq!(request, b"test request"); - let res = AdexResponse::Ok { response: b"test response".to_vec(), }; diff --git a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs index 412fa16355..1bede91995 100644 --- a/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs +++ b/mm2src/mm2_p2p/src/behaviours/peers_exchange.rs @@ -330,11 +330,18 @@ impl PeersExchange { pub fn get_random_peers( &mut self, num: usize, - mut filter: impl FnMut(&PeerId) -> bool, + mut filter: impl FnMut(&PeerId, HashSet) -> bool, ) -> HashMap { let mut result = HashMap::with_capacity(num); let mut rng = rand::thread_rng(); - let peer_ids = self.known_peers.iter().filter(|peer| filter(peer)).collect::>(); + let peer_ids = self + .known_peers + .iter() + .filter(|peer| { + let addresses = self.request_response.addresses_of_peer(peer).into_iter().collect(); + filter(peer, addresses) + }) + .collect::>(); for peer_id in peer_ids.choose_multiple(&mut rng, num) { let addresses = self.request_response.addresses_of_peer(peer_id).into_iter().collect(); diff --git a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json index ee56de9d79..4400c318e0 100644 --- a/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json +++ b/mm2src/mm2_test_helpers/dummy_files/nucleus-history.json @@ -1,4 +1,56 @@ [ + { + "tx_hex": "0a0f30372d74656e6465726d696e742d321288070a262f6962632e6c69676874636c69656e74732e74656e6465726d696e742e76312e48656164657212dd060ad5040a98030a02080b1211636f736d6f736875622d746573746e657418df06220c08efe4c2b80610b7c1ffe8022a480a201b8fdfca293c20e10c86c89ce5a66cedd40cbf83f3833e504844fb89f6606d0b122408011220e58f2ab7b93c559d54eb0f62111fd5c065121ef09e93be370a523af1620f2c3732201692b08be7826c81b282e7f1ef95b75a3e59e824827fab5fe3419b40ba4b7e2f3a20cf0ff3ec944444eef5c483642c33019c7f1c6dd168bcb0cf70dc4a7606edb94b42209293a1e6e3030b68610f5d4eaff415483ba6d7d493b6d86ea22cb25bea4258bb4a209293a1e6e3030b68610f5d4eaff415483ba6d7d493b6d86ea22cb25bea4258bb5220048091bc7ddc283f77bfbf91d73c44da58c3df8a9cbc867405d8b7f3daada22f5a205dc49011b034d2315b08ec147193cf45dadd9133de1f4aaeea2098068b9c9d256220e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b8556a20e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b85572143e49b197ac395db2bd999b4c3f35f1b6b36508d012b70108df061a480a207348ea274d50794629bb1e196d0280dfe01588687ae576f320f2e0e4c4be897c122408011220a01fbeb8a9a603cc741f7a44f050e0c9694adba7772738e6f7599019f6f7f1072268080212143e49b197ac395db2bd999b4c3f35f1b6b36508d01a0c08f4e4c2b8061094d8feeb022240bf027c9e216ed59b699a3e09ba5e09397ed401e025690463b3fce0421c917b144c335b544e994482a2160a1fb10fd7dd10b8dff5bb24e625652f3187c0feb90d127e0a3d0a143e49b197ac395db2bd999b4c3f35f1b6b36508d012220a208c9bbd034ed5ccfeb619b9588266c11de77c0e59853558368c468cd29e852d0518c801123d0a143e49b197ac395db2bd999b4c3f35f1b6b36508d012220a208c9bbd034ed5ccfeb619b9588266c11de77c0e59853558368c468cd29e852d0518c8011a03109c06227e0a3d0a143e49b197ac395db2bd999b4c3f35f1b6b36508d012220a208c9bbd034ed5ccfeb619b9588266c11de77c0e59853558368c468cd29e852d0518c801123d0a143e49b197ac395db2bd999b4c3f35f1b6b36508d012220a208c9bbd034ed5ccfeb619b9588266c11de77c0e59853558368c468cd29e852d0518c8011a2a6e7563313232726b6e3578306b677a72787a7964797966766c6e6e653433716a387578393866677a7470", + "tx_hash": "BF87EAB193BC8E1C8975105C165497DDC4AE4D5378F50AECBA38D43B715D96F3", + "from": [ + "nuc12k2pyuylm9t7ugdvz67h9pg4gmmvhn5v77y0sd" + ], + "to": [ + "nuc1z609g9z8ef4jlkcfgg36c2ljgcmna50l77n078" + ], + "total_amount": "0.615", + "spent_by_me": "0", + "received_by_me": "0.615", + "my_balance_change": "0.615", + "block_height": 1183, + "timestamp": 1729147508, + "fee_details": { + "type": "Tendermint", + "coin": "NUCLEUS-TEST", + "amount": "0.12201", + "gas_limit": 125000 + }, + "coin": "NUCLEUS-TEST", + "internal_id": "4335303135373938433145384342333931424145373846420000000000000000", + "transaction_type": "StandardTransfer", + "memo": "rly(2.5.2-28-gdf42391)" + }, + { + "tx_hex": "0a087472616e7366657212096368616e6e656c2d321a0f0a05756e75636c1206363135303030222a6e7563317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c37376e3037382a2d636f736d6f7331333468397476373836366a63757737303877357737366c6366783773337832797379616c787938f3edc3aadad1c9ff17", + "tx_hash": "95BA748DC10BE865C1EBEA10A433E8E5F626E14474676037374539C6FBC74655", + "from": [ + "nuc1z609g9z8ef4jlkcfgg36c2ljgcmna50l77n078" + ], + "to": [ + "cosmos134h9tv7866jcuw708w5w76lcfx7s3x2ysyalxy" + ], + "total_amount": "0.651435", + "spent_by_me": "0.651435", + "received_by_me": "0", + "my_balance_change": "-0.651435", + "block_height": 1128, + "timestamp": 1729142296, + "fee_details": { + "type": "Tendermint", + "coin": "NUCLEUS-TEST", + "amount": "0.036435", + "gas_limit": 125000 + }, + "coin": "NUCLEUS-TEST", + "internal_id": "3031414542453143353638454230314344383437414235390000000000000000", + "transaction_type": "StandardTransfer", + "memo": "" + }, { "tx_hex": "0a2a6e7563317a36303967397a386566346a6c6b63666767333663326c6a67636d6e6135306c37376e3037381240303932443741333142373239304433434438384231414338344336433338313137344134444638393046323831343232343335373334413145323534344641461a4039393836333361343532623636373561663632626261666364646630376461336264343633343262623935373935643637346335373162613363666661386433", "tx_hash": "17C70B8D8B4DA83F5C625A32DAD2BD2572F0770D6F7AD52284781E71C20F89E4", diff --git a/mm2src/mm2_test_helpers/src/for_tests.rs b/mm2src/mm2_test_helpers/src/for_tests.rs index 880edc777a..b367c4653c 100644 --- a/mm2src/mm2_test_helpers/src/for_tests.rs +++ b/mm2src/mm2_test_helpers/src/for_tests.rs @@ -241,7 +241,11 @@ pub const ETH_MAINNET_NODE: &str = "https://mainnet.infura.io/v3/c01c1b4cf666425 pub const ETH_MAINNET_CHAIN_ID: u64 = 1; pub const ETH_MAINNET_SWAP_CONTRACT: &str = "0x24abe4c71fc658c91313b6552cd40cd808b3ea80"; -pub const ETH_SEPOLIA_NODES: &[&str] = &["https://ethereum-sepolia-rpc.publicnode.com","https://rpc2.sepolia.org","https://1rpc.io/sepolia"]; +pub const ETH_SEPOLIA_NODES: &[&str] = &[ + "https://ethereum-sepolia-rpc.publicnode.com", + "https://rpc2.sepolia.org", + "https://1rpc.io/sepolia", +]; pub const ETH_SEPOLIA_CHAIN_ID: u64 = 11155111; pub const ETH_SEPOLIA_SWAP_CONTRACT: &str = "0xeA6D65434A15377081495a9E7C5893543E7c32cB"; pub const ETH_SEPOLIA_TOKEN_CONTRACT: &str = "0x09d0d71FBC00D7CCF9CFf132f5E6825C88293F19"; @@ -1129,10 +1133,16 @@ pub fn mm_ctx_with_custom_db_with_conf(conf: Option) -> MmArc { let ctx = ctx_builder.into_mm_arc(); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.sqlite_connection.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .sqlite_connection + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); let connection = Connection::open_in_memory().unwrap(); - let _ = ctx.shared_sqlite_conn.pin(Arc::new(Mutex::new(connection))); + let _ = ctx + .shared_sqlite_conn + .set(Arc::new(Mutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1146,7 +1156,10 @@ pub async fn mm_ctx_with_custom_async_db() -> MmArc { let ctx = MmCtxBuilder::new().into_mm_arc(); let connection = AsyncConnection::open_in_memory().await.unwrap(); - let _ = ctx.async_sqlite_connection.pin(Arc::new(AsyncMutex::new(connection))); + let _ = ctx + .async_sqlite_connection + .set(Arc::new(AsyncMutex::new(connection))) + .map_err(|_| "Already Initialized".to_string()); ctx } @@ -1428,8 +1441,7 @@ impl MarketMakerIt { } let ctx = { - let builder = MmCtxBuilder::new() - .with_conf(conf.clone()); + let builder = MmCtxBuilder::new().with_conf(conf.clone()); let builder = if let Some(ns) = db_namespace_id { builder.with_test_db_namespace_with_id(ns) @@ -1522,7 +1534,7 @@ impl MarketMakerIt { let wasm_rpc = self .ctx .wasm_rpc - .as_option() + .get() .expect("'MmCtx::rpc' must be initialized already"); match wasm_rpc.request(payload.clone()).await { // Please note a new type of error will be introduced soon. @@ -3079,6 +3091,33 @@ pub async fn enable_tendermint_token(mm: &MarketMakerIt, coin: &str) -> Json { json::from_str(&request.1).unwrap() } +pub async fn tendermint_validators( + mm: &MarketMakerIt, + coin: &str, + filter_by_status: &str, + limit: usize, + page_number: usize, +) -> Json { + let rpc_endpoint = "tendermint_validators"; + let request = json!({ + "userpass": mm.userpass, + "method": rpc_endpoint, + "mmrpc": "2.0", + "params": { + "ticker": coin, + "filter_by_status": filter_by_status, + "limit": limit, + "page_number": page_number + } + }); + log!("{rpc_endpoint} request {}", json::to_string(&request).unwrap()); + + let response = mm.rpc(&request).await.unwrap(); + assert_eq!(response.0, StatusCode::OK, "{rpc_endpoint} failed: {}", response.1); + log!("{rpc_endpoint} response {}", response.1); + json::from_str(&response.1).unwrap() +} + pub async fn init_utxo_electrum( mm: &MarketMakerIt, coin: &str, @@ -3253,6 +3292,99 @@ pub async fn enable_eth_with_tokens_v2( } } +async fn init_erc20_token( + mm: &MarketMakerIt, + ticker: &str, + protocol: Option, + path_to_address: Option, +) -> Result<(StatusCode, Json), Json> { + let (status, response, _) = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_erc20::init", + "mmrpc": "2.0", + "params": { + "ticker": ticker, + "protocol": protocol, + "activation_params": { + "path_to_address": path_to_address.unwrap_or_default(), + } + } + })) + .await + .unwrap(); + + if status.is_success() { + Ok((status, json::from_str(&response).unwrap())) + } else { + Err(json::from_str(&response).unwrap()) + } +} + +async fn init_erc20_token_status(mm: &MarketMakerIt, task_id: u64) -> Json { + let request = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "task::enable_erc20::status", + "mmrpc": "2.0", + "params": { + "task_id": task_id, + } + })) + .await + .unwrap(); + assert_eq!( + request.0, + StatusCode::OK, + "'task::enable_erc20::status' failed: {}", + request.1 + ); + json::from_str(&request.1).unwrap() +} + +pub async fn enable_erc20_token_v2( + mm: &MarketMakerIt, + ticker: &str, + protocol: Option, + timeout: u64, + path_to_address: Option, +) -> Result { + let init = init_erc20_token(mm, ticker, protocol, path_to_address).await?.1; + let init: RpcV2Response = json::from_value(init).unwrap(); + let timeout = wait_until_ms(timeout * 1000); + + loop { + if now_ms() > timeout { + panic!("{} initialization timed out", ticker); + } + + let status = init_erc20_token_status(mm, init.result.task_id).await; + let status: RpcV2Response = json::from_value(status).unwrap(); + match status.result { + InitErc20TokenStatus::Ok(result) => break Ok(result), + InitErc20TokenStatus::Error(e) => break Err(e), + _ => Timer::sleep(1.).await, + } + } +} + +pub async fn get_token_info(mm: &MarketMakerIt, protocol: Json) -> TokenInfoResponse { + let response = mm + .rpc(&json!({ + "userpass": mm.userpass, + "method": "get_token_info", + "mmrpc": "2.0", + "params": { + "protocol": protocol, + } + })) + .await + .unwrap(); + assert_eq!(response.0, StatusCode::OK, "'get_token_info' failed: {}", response.1); + let response_json: Json = json::from_str(&response.1).unwrap(); + json::from_value(response_json["result"].clone()).unwrap() +} + /// Note that mm2 ignores `volume` if `max` is true. pub async fn set_price( mm: &MarketMakerIt, diff --git a/mm2src/mm2_test_helpers/src/structs.rs b/mm2src/mm2_test_helpers/src/structs.rs index 96cad93739..baba173461 100644 --- a/mm2src/mm2_test_helpers/src/structs.rs +++ b/mm2src/mm2_test_helpers/src/structs.rs @@ -714,6 +714,15 @@ pub enum InitEthWithTokensStatus { UserActionRequired(Json), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, tag = "status", content = "details")] +pub enum InitErc20TokenStatus { + Ok(InitTokenActivationResult), + Error(Json), + InProgress(Json), + UserActionRequired(Json), +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields, tag = "status", content = "details")] pub enum InitLightningStatus { @@ -911,6 +920,17 @@ pub enum EthWithTokensActivationResult { HD(HDEthWithTokensActivationResult), } +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct InitTokenActivationResult { + pub ticker: String, + pub platform_coin: String, + pub token_contract_address: String, + pub current_block: u64, + pub required_confirmations: u64, + pub wallet_balance: EnableCoinBalanceMap, +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct EnableBchWithTokensResponse { @@ -1186,3 +1206,25 @@ pub struct ActiveSwapsResponse { pub uuids: Vec, pub statuses: Option>, } + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Erc20TokenInfo { + pub symbol: String, + pub decimals: u8, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(tag = "type", content = "info")] +pub enum TokenInfo { + ERC20(Erc20TokenInfo), +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct TokenInfoResponse { + pub config_ticker: Option, + #[serde(flatten)] + pub info: TokenInfo, +} diff --git a/mm2src/proxy_signature/Cargo.toml b/mm2src/proxy_signature/Cargo.toml index 5392b9862a..bbad60cbb8 100644 --- a/mm2src/proxy_signature/Cargo.toml +++ b/mm2src/proxy_signature/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" [dependencies] chrono = "0.4" http = "0.2" -libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.4", default-features = false, features = ["identify"] } +libp2p = { git = "https://github.com/KomodoPlatform/rust-libp2p.git", tag = "k-0.52.11", default-features = false, features = ["identify"] } serde = "1" serde_json = { version = "1", features = ["preserve_order", "raw_value"] } diff --git a/mm2src/trading_api/Cargo.toml b/mm2src/trading_api/Cargo.toml new file mode 100644 index 0000000000..4fd9514fb9 --- /dev/null +++ b/mm2src/trading_api/Cargo.toml @@ -0,0 +1,28 @@ +[package] +# integration with external trading api +name = "trading_api" +version = "0.1.0" +edition = "2018" + +[dependencies] +common = { path = "../common" } +enum_derives = { path = "../derives/enum_derives" } +mm2_core = { path = "../mm2_core" } +mm2_err_handle = { path = "../mm2_err_handle" } +mm2_net = { path = "../mm2_net" } +mm2_number = { path = "../mm2_number" } +mocktopus = { version = "0.8.0", optional = true } + +derive_more = "0.99" +ethereum-types = { version = "0.13", default-features = false, features = ["std", "serialize"] } +lazy_static = "1.4" +serde = "1.0" +serde_derive = "1.0" +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } +url = { version = "2.2.2", features = ["serde"] } + +[features] +test-ext-api = [] # use test config to connect to an external api + +[dev-dependencies] +mocktopus = { version = "0.8.0" } \ No newline at end of file diff --git a/mm2src/trading_api/src/lib.rs b/mm2src/trading_api/src/lib.rs new file mode 100644 index 0000000000..183e6d9bcd --- /dev/null +++ b/mm2src/trading_api/src/lib.rs @@ -0,0 +1,3 @@ +//! This module is for indirect connection to third-party trading APIs, processing their results and errors + +pub mod one_inch_api; diff --git a/mm2src/trading_api/src/one_inch_api.rs b/mm2src/trading_api/src/one_inch_api.rs new file mode 100644 index 0000000000..9b0af1625e --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api.rs @@ -0,0 +1,5 @@ +//! Wrapper for 1inch API. + +pub mod client; +pub mod errors; +pub mod types; diff --git a/mm2src/trading_api/src/one_inch_api/client.rs b/mm2src/trading_api/src/one_inch_api/client.rs new file mode 100644 index 0000000000..ef3c61ef6b --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/client.rs @@ -0,0 +1,176 @@ +use super::errors::ApiClientError; +use crate::one_inch_api::errors::NativeError; +use common::StatusCode; +#[cfg(feature = "test-ext-api")] use lazy_static::lazy_static; +use mm2_core::mm_ctx::MmArc; +use mm2_err_handle::{map_mm_error::MapMmError, + map_to_mm::MapToMmResult, + mm_error::{MmError, MmResult}}; +use mm2_net::transport::slurp_url_with_headers; +use serde::de::DeserializeOwned; +use url::Url; + +#[cfg(any(test, feature = "mocktopus"))] +use mocktopus::macros::*; + +const ONE_INCH_API_ENDPOINT_V6_0: &str = "swap/v6.0/"; +const SWAP_METHOD: &str = "swap"; +const QUOTE_METHOD: &str = "quote"; +const LIQUIDITY_SOURCES_METHOD: &str = "liquidity-sources"; +const TOKENS_METHOD: &str = "tokens"; + +const ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0: &str = "0x111111125421ca6dc452d289314280a0f8842a65"; +const ONE_INCH_ETH_SPECIAL_CONTRACT: &str = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; + +#[cfg(test)] +const ONE_INCH_API_TEST_URL: &str = "https://api.1inch.dev"; + +#[cfg(feature = "test-ext-api")] +lazy_static! { + /// API key for testing + static ref ONE_INCH_API_TEST_AUTH: String = std::env::var("ONE_INCH_API_TEST_AUTH").unwrap_or_default(); +} + +pub(crate) type QueryParams<'life> = Vec<(&'life str, String)>; + +/// 1inch v6.0 supported eth-based chains +const ONE_INCH_V6_0_SUPPORTED_CHAINS: &[(&str, u64)] = &[ + ("Ethereum", 1), + ("Optimism", 10), + ("BSC", 56), + ("Gnosis", 100), + ("Polygon", 137), + ("Fantom", 250), + ("ZkSync", 324), + ("Klaytn", 8217), + ("Base", 8453), + ("Arbitrum", 42161), + ("Avalanche", 43114), + ("Aurora", 1313161554), +]; + +pub(crate) struct UrlBuilder<'a> { + base_url: Url, + endpoint: &'a str, + chain_id: u64, + method_name: String, + query_params: QueryParams<'a>, +} + +impl<'a> UrlBuilder<'a> { + pub(crate) fn new(api_client: &ApiClient, chain_id: u64, method_name: String) -> Self { + Self { + base_url: api_client.base_url.clone(), + endpoint: ApiClient::get_swap_endpoint(), + chain_id, + method_name, + query_params: vec![], + } + } + + pub(crate) fn with_query_params(&mut self, mut more_params: QueryParams<'a>) -> &mut Self { + self.query_params.append(&mut more_params); + self + } + + #[allow(clippy::result_large_err)] + pub(crate) fn build(&self) -> MmResult { + let url = self + .base_url + .join(self.endpoint)? + .join(&format!("{}/", self.chain_id))? + .join(self.method_name.as_str())?; + Ok(Url::parse_with_params( + url.as_str(), + self.query_params + .iter() + .map(|v| (v.0, v.1.as_str())) + .collect::>(), + )?) + } +} + +/// 1-inch API caller +pub struct ApiClient { + base_url: Url, +} + +#[allow(clippy::swap_ptr_to_ref)] // need for moctopus +#[cfg_attr(any(test, feature = "mocktopus"), mockable)] +impl ApiClient { + #[allow(unused_variables)] + #[allow(clippy::result_large_err)] + pub fn new(ctx: MmArc) -> MmResult { + #[cfg(not(test))] + let url_cfg = ctx.conf["1inch_api"] + .as_str() + .ok_or(ApiClientError::InvalidParam("No API config param".to_owned()))?; + + #[cfg(test)] + let url_cfg = ONE_INCH_API_TEST_URL; + + Ok(Self { + base_url: Url::parse(url_cfg)?, + }) + } + + pub const fn eth_special_contract() -> &'static str { ONE_INCH_ETH_SPECIAL_CONTRACT } + + pub const fn classic_swap_contract() -> &'static str { ONE_INCH_AGGREGATION_ROUTER_CONTRACT_V6_0 } + + pub fn is_chain_supported(chain_id: u64) -> bool { + ONE_INCH_V6_0_SUPPORTED_CHAINS.iter().any(|(_name, id)| *id == chain_id) + } + + fn get_headers() -> Vec<(&'static str, &'static str)> { + vec![ + #[cfg(feature = "test-ext-api")] + ("Authorization", ONE_INCH_API_TEST_AUTH.as_str()), + ("accept", "application/json"), + ] + } + + fn get_swap_endpoint() -> &'static str { ONE_INCH_API_ENDPOINT_V6_0 } + + pub const fn get_swap_method() -> &'static str { SWAP_METHOD } + + pub const fn get_quote_method() -> &'static str { QUOTE_METHOD } + + pub const fn get_liquidity_sources_method() -> &'static str { LIQUIDITY_SOURCES_METHOD } + + pub const fn get_tokens_method() -> &'static str { TOKENS_METHOD } + + pub(crate) async fn call_api(api_url: &Url) -> MmResult { + let (status_code, _, body) = slurp_url_with_headers(api_url.as_str(), ApiClient::get_headers()) + .await + .mm_err(ApiClientError::TransportError)?; + let body = serde_json::from_slice(&body).map_to_mm(|err| ApiClientError::ParseBodyError { + error_msg: err.to_string(), + })?; + if status_code != StatusCode::OK { + let error = NativeError::new(status_code, body); + return Err(MmError::new(ApiClientError::from_native_error(error))); + } + serde_json::from_value(body).map_err(|err| { + ApiClientError::ParseBodyError { + error_msg: err.to_string(), + } + .into() + }) + } + + pub async fn call_swap_api<'l, T: DeserializeOwned>( + &self, + chain_id: u64, + method: String, + params: Option>, + ) -> MmResult { + let mut builder = UrlBuilder::new(self, chain_id, method); + if let Some(params) = params { + builder.with_query_params(params); + } + let api_url = builder.build()?; + + ApiClient::call_api(&api_url).await + } +} diff --git a/mm2src/trading_api/src/one_inch_api/errors.rs b/mm2src/trading_api/src/one_inch_api/errors.rs new file mode 100644 index 0000000000..70264a9b89 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/errors.rs @@ -0,0 +1,130 @@ +use common::StatusCode; +use derive_more::Display; +use enum_derives::EnumFromStringify; +use ethereum_types::U256; +use mm2_net::transport::SlurpError; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Display, Serialize, EnumFromStringify)] +pub enum ApiClientError { + #[from_stringify("url::ParseError")] + InvalidParam(String), + #[display(fmt = "Parameter {param} out of bounds, value: {value}, min: {min} max: {max}")] + OutOfBounds { + param: String, + value: String, + min: String, + max: String, + }, + TransportError(SlurpError), + ParseBodyError { + error_msg: String, + }, + #[display(fmt = "General API error: {error_msg} description: {description}")] + GeneralApiError { + error_msg: String, + description: String, + status_code: u16, + }, + #[display(fmt = "Allowance not enough, needed: {amount} allowance: {allowance}")] + AllowanceNotEnough { + error_msg: String, + description: String, + status_code: u16, + /// Amount to approve for the API contract + amount: U256, + /// Existing allowance for the API contract + allowance: U256, + }, +} + +// API error meta 'type' field known values +const META_TYPE_ALLOWANCE: &str = "allowance"; +const META_TYPE_AMOUNT: &str = "amount"; + +#[derive(Debug, Deserialize)] +pub(crate) struct Error400 { + pub error: String, + pub description: Option, + #[serde(rename = "statusCode")] + pub status_code: u16, + pub meta: Option>, + #[allow(dead_code)] + #[serde(rename = "requestId")] + pub request_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Meta { + #[serde(rename = "type")] + pub meta_type: String, + #[serde(rename = "value")] + pub meta_value: String, +} + +#[derive(Debug)] +pub(crate) enum NativeError { + HttpError { error_msg: String, status_code: u16 }, + HttpError400(Error400), + ParseError { error_msg: String }, +} + +impl NativeError { + pub(crate) fn new(status_code: StatusCode, body: Value) -> Self { + if status_code == StatusCode::BAD_REQUEST { + match serde_json::from_value(body) { + Ok(err) => Self::HttpError400(err), + Err(err) => Self::ParseError { + error_msg: format!("could not parse error response: {}", err), + }, + } + } else { + Self::HttpError { + error_msg: body["error"].as_str().unwrap_or_default().to_owned(), + status_code: status_code.into(), + } + } + } +} + +impl ApiClientError { + /// Convert from native API errors to lib errors + /// Look for known API errors. If none found return as general API error + pub(crate) fn from_native_error(api_error: NativeError) -> ApiClientError { + match api_error { + NativeError::HttpError400(error_400) => { + if let Some(meta) = error_400.meta { + // Try if it's "Not enough allowance" error 'meta' data: + if let Some(meta_allowance) = meta.iter().find(|m| m.meta_type == META_TYPE_ALLOWANCE) { + // try find 'amount' value + let amount = if let Some(meta_amount) = meta.iter().find(|m| m.meta_type == META_TYPE_AMOUNT) { + U256::from_dec_str(&meta_amount.meta_value).unwrap_or_default() + } else { + Default::default() + }; + let allowance = U256::from_dec_str(&meta_allowance.meta_value).unwrap_or_default(); + return ApiClientError::AllowanceNotEnough { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + amount, + allowance, + }; + } + } + ApiClientError::GeneralApiError { + error_msg: error_400.error, + status_code: error_400.status_code, + description: error_400.description.unwrap_or_default(), + } + }, + NativeError::HttpError { error_msg, status_code } => ApiClientError::GeneralApiError { + error_msg, + status_code, + description: Default::default(), + }, + NativeError::ParseError { error_msg } => ApiClientError::ParseBodyError { error_msg }, + } + } +} diff --git a/mm2src/trading_api/src/one_inch_api/types.rs b/mm2src/trading_api/src/one_inch_api/types.rs new file mode 100644 index 0000000000..f13e943768 --- /dev/null +++ b/mm2src/trading_api/src/one_inch_api/types.rs @@ -0,0 +1,411 @@ +#![allow(clippy::result_large_err)] + +use super::client::QueryParams; +use super::errors::ApiClientError; +use common::{def_with_opt_param, push_if_some}; +use ethereum_types::Address; +use mm2_err_handle::mm_error::{MmError, MmResult}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +const ONE_INCH_MAX_SLIPPAGE: f32 = 50.0; +const ONE_INCH_MAX_FEE_SHARE: f32 = 3.0; +const ONE_INCH_MAX_GAS: u128 = 11500000; +const ONE_INCH_MAX_PARTS: u32 = 100; +const ONE_INCH_MAX_MAIN_ROUTE_PARTS: u32 = 50; +const ONE_INCH_MAX_COMPLEXITY_LEVEL: u32 = 3; + +const BAD_URL_IN_RESPONSE_ERROR: &str = "unsupported url in response"; +const ONE_INCH_DOMAIN: &str = "1inch.io"; + +/// API params builder for swap quote +#[derive(Default)] +pub struct ClassicSwapQuoteParams { + /// Source token address + src: String, + /// Destination token address + dst: String, + amount: String, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, +} + +impl ClassicSwapQuoteParams { + pub fn new(src: String, dst: String, amount: String) -> Self { + Self { + src, + dst, + amount, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +/// API params builder to create a tx for swap +#[derive(Default)] +pub struct ClassicSwapCreateParams { + src: String, + dst: String, + amount: String, + from: String, + slippage: f32, + // Optional fields + fee: Option, + protocols: Option, + gas_price: Option, + complexity_level: Option, + parts: Option, + main_route_parts: Option, + gas_limit: Option, + include_tokens_info: Option, + include_protocols: Option, + include_gas: Option, + connector_tokens: Option, + excluded_protocols: Option, + permit: Option, + compatibility: Option, + receiver: Option, + referrer: Option, + disable_estimate: Option, + allow_partial_fill: Option, + use_permit2: Option, +} + +impl ClassicSwapCreateParams { + pub fn new(src: String, dst: String, amount: String, from: String, slippage: f32) -> Self { + Self { + src, + dst, + amount, + from, + slippage, + ..Default::default() + } + } + + def_with_opt_param!(fee, f32); + def_with_opt_param!(protocols, String); + def_with_opt_param!(gas_price, String); + def_with_opt_param!(complexity_level, u32); + def_with_opt_param!(parts, u32); + def_with_opt_param!(main_route_parts, u32); + def_with_opt_param!(gas_limit, u128); + def_with_opt_param!(include_tokens_info, bool); + def_with_opt_param!(include_protocols, bool); + def_with_opt_param!(include_gas, bool); + def_with_opt_param!(connector_tokens, String); + def_with_opt_param!(excluded_protocols, String); + def_with_opt_param!(permit, String); + def_with_opt_param!(compatibility, bool); + def_with_opt_param!(receiver, String); + def_with_opt_param!(referrer, String); + def_with_opt_param!(disable_estimate, bool); + def_with_opt_param!(allow_partial_fill, bool); + def_with_opt_param!(use_permit2, bool); + + pub fn build_query_params(&self) -> MmResult, ApiClientError> { + self.validate_params()?; + + let mut params = vec![ + ("src", self.src.clone()), + ("dst", self.dst.clone()), + ("amount", self.amount.clone()), + ("from", self.from.clone()), + ("slippage", self.slippage.to_string()), + ]; + + push_if_some!(params, "fee", self.fee); + push_if_some!(params, "protocols", &self.protocols); + push_if_some!(params, "gasPrice", &self.gas_price); + push_if_some!(params, "complexityLevel", self.complexity_level); + push_if_some!(params, "parts", self.parts); + push_if_some!(params, "mainRouteParts", self.main_route_parts); + push_if_some!(params, "gasLimit", self.gas_limit); + push_if_some!(params, "includeTokensInfo", self.include_tokens_info); + push_if_some!(params, "includeProtocols", self.include_protocols); + push_if_some!(params, "includeGas", self.include_gas); + push_if_some!(params, "connectorTokens", &self.connector_tokens); + push_if_some!(params, "excludedProtocols", &self.excluded_protocols); + push_if_some!(params, "permit", &self.permit); + push_if_some!(params, "compatibility", &self.compatibility); + push_if_some!(params, "receiver", &self.receiver); + push_if_some!(params, "referrer", &self.referrer); + push_if_some!(params, "disableEstimate", self.disable_estimate); + push_if_some!(params, "allowPartialFill", self.allow_partial_fill); + push_if_some!(params, "usePermit2", self.use_permit2); + + Ok(params) + } + + /// Validate params by 1inch rules (to avoid extra requests) + fn validate_params(&self) -> MmResult<(), ApiClientError> { + validate_slippage(self.slippage)?; + validate_fee(&self.fee)?; + validate_complexity_level(&self.complexity_level)?; + validate_gas_limit(&self.gas_limit)?; + validate_parts(&self.parts)?; + validate_main_route_parts(&self.main_route_parts)?; + Ok(()) + } +} + +#[derive(Deserialize, Debug, Serialize)] +pub struct TokenInfo { + pub address: Address, + pub symbol: String, + pub name: String, + pub decimals: u32, + pub eip2612: bool, + #[serde(rename = "isFoT", default)] + pub is_fot: bool, + #[serde(rename = "logoURI", with = "serde_one_inch_link")] + pub logo_uri: String, + pub tags: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ProtocolInfo { + pub name: String, + pub part: f64, + #[serde(rename = "fromTokenAddress")] + pub from_token_address: Address, + #[serde(rename = "toTokenAddress")] + pub to_token_address: Address, +} + +#[derive(Deserialize, Debug)] +pub struct ClassicSwapData { + /// dst token amount to receive, in api is a decimal number as string + #[serde(rename = "dstAmount")] + pub dst_amount: String, + #[serde(rename = "srcToken")] + pub src_token: Option, + #[serde(rename = "dstToken")] + pub dst_token: Option, + pub protocols: Option>>>, + pub tx: Option, + pub gas: Option, +} + +#[derive(Deserialize, Debug)] +pub struct TxFields { + pub from: Address, + pub to: Address, + pub data: String, + /// tx value, in api is a decimal number as string + pub value: String, + /// gas price, in api is a decimal number as string + #[serde(rename = "gasPrice")] + pub gas_price: String, + /// gas limit, in api is a decimal number + pub gas: u128, +} + +#[derive(Deserialize, Serialize)] +pub struct ProtocolImage { + pub id: String, + pub title: String, + #[serde(with = "serde_one_inch_link")] + pub img: String, + #[serde(with = "serde_one_inch_link")] + pub img_color: String, +} + +#[derive(Deserialize)] +pub struct ProtocolsResponse { + pub protocols: Vec, +} + +#[derive(Deserialize)] +pub struct TokensResponse { + pub tokens: HashMap, +} + +mod serde_one_inch_link { + use super::validate_one_inch_link; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + /// Just forward to the normal serializer + pub(super) fn serialize(s: &String, serializer: S) -> Result + where + S: Serializer, + { + s.serialize(serializer) + } + + /// Deserialise String with checking links + pub(super) fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + ::deserialize(deserializer) + .map(|value| validate_one_inch_link(&value).unwrap_or_default()) + } +} + +fn validate_slippage(slippage: f32) -> MmResult<(), ApiClientError> { + if !(0.0..=ONE_INCH_MAX_SLIPPAGE).contains(&slippage) { + return Err(ApiClientError::OutOfBounds { + param: "slippage".to_owned(), + value: slippage.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_SLIPPAGE.to_string(), + } + .into()); + } + Ok(()) +} + +fn validate_fee(fee: &Option) -> MmResult<(), ApiClientError> { + if let Some(fee) = fee { + if !(0.0..=ONE_INCH_MAX_FEE_SHARE).contains(fee) { + return Err(ApiClientError::OutOfBounds { + param: "fee".to_owned(), + value: fee.to_string(), + min: 0.0.to_string(), + max: ONE_INCH_MAX_FEE_SHARE.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_gas_limit(gas_limit: &Option) -> MmResult<(), ApiClientError> { + if let Some(gas_limit) = gas_limit { + if gas_limit > &ONE_INCH_MAX_GAS { + return Err(ApiClientError::OutOfBounds { + param: "gas_limit".to_owned(), + value: gas_limit.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_GAS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_parts(parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(parts) = parts { + if parts > &ONE_INCH_MAX_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "parts".to_owned(), + value: parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_main_route_parts(main_route_parts: &Option) -> MmResult<(), ApiClientError> { + if let Some(main_route_parts) = main_route_parts { + if main_route_parts > &ONE_INCH_MAX_MAIN_ROUTE_PARTS { + return Err(ApiClientError::OutOfBounds { + param: "main route parts".to_owned(), + value: main_route_parts.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_MAIN_ROUTE_PARTS.to_string(), + } + .into()); + } + } + Ok(()) +} + +fn validate_complexity_level(complexity_level: &Option) -> MmResult<(), ApiClientError> { + if let Some(complexity_level) = complexity_level { + if complexity_level > &ONE_INCH_MAX_COMPLEXITY_LEVEL { + return Err(ApiClientError::OutOfBounds { + param: "complexity level".to_owned(), + value: complexity_level.to_string(), + min: 0.to_string(), + max: ONE_INCH_MAX_COMPLEXITY_LEVEL.to_string(), + } + .into()); + } + } + Ok(()) +} + +/// Check if url is valid and is a subdomain of 1inch domain (simple anti-phishing check) +fn validate_one_inch_link(s: &str) -> MmResult { + let url = Url::parse(s).map_err(|_err| ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + })?; + if let Some(host) = url.host() { + if host.to_string().ends_with(ONE_INCH_DOMAIN) { + return Ok(s.to_owned()); + } + } + MmError::err(ApiClientError::ParseBodyError { + error_msg: BAD_URL_IN_RESPONSE_ERROR.to_owned(), + }) +} + +#[test] +fn test_validate_one_inch_link() { + assert!(validate_one_inch_link("https://cdn.1inch.io/liquidity-sources-logo/wmatic_color.png").is_ok()); + assert!(validate_one_inch_link("https://example.org/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("https://inch.io/somepath/somefile.png").is_err()); + assert!(validate_one_inch_link("127.0.0.1").is_err()); +}