Pārlūkot izejas kodu

feat(fortuna): Improve explorer (#2717)

* Revert "feat(fortuna): extract RETRY_PREVIOUS_BLOCKS into EthereumConfig (#2713)"

This reverts commit 0b1a59f63f47d93ba7bb149e0558f869d20c8d73.

* optional provider random number in case of failure

* add gas

* more fields

* limit and offset

* add network id as the main identifier

* bump
Amin Moghaddam 6 mēneši atpakaļ
vecāks
revīzija
83786988a7
28 mainītis faili ar 606 papildinājumiem un 225 dzēšanām
  1. 12 0
      apps/fortuna/.sqlx/query-03901bcfb28b127d99fe8a53e480b88336dd2aab632411114f02ce8dd8fe07e8.json
  2. 0 12
      apps/fortuna/.sqlx/query-16635b3d9c6f9b743614e0e08bfa2b26d7ec6346f0323d9f16b98c32fd9a91f6.json
  3. 35 17
      apps/fortuna/.sqlx/query-392da9e5fdd212a4a665c86e5fc6d4f619355294490248e656ad0fc97a252471.json
  4. 3 3
      apps/fortuna/.sqlx/query-4c8c05ec08e128d847faafdd3d79fa50da70066f30b74f354e5d3a843ba6a2c0.json
  5. 35 17
      apps/fortuna/.sqlx/query-78be8c62d5eb764995221f927b0f166e38d6fba8eb8fddb07f50c572fd27b4e2.json
  6. 32 14
      apps/fortuna/.sqlx/query-8cd10cd5839b81bd9538aeb10fdfd27c6e36baf5d90a4fb9e61718f021812710.json
  7. 32 14
      apps/fortuna/.sqlx/query-905dbc91cd5319537c5c194277d531689ac5c1338396414467496d0f50ddc3f0.json
  8. 32 14
      apps/fortuna/.sqlx/query-a62e094cee65ae58bd12ce7d3e7df44f5aca31520d1ceced83f492945e850764.json
  9. 12 0
      apps/fortuna/.sqlx/query-b0d9afebb3825c3509ad80e5ebab5d72360326593407518770fe537ac3da1e10.json
  10. 0 12
      apps/fortuna/.sqlx/query-b2baa9f9d46f873a3a7117c38ecab09f56082c5267dbf5180f39c608b6262f5a.json
  11. 34 16
      apps/fortuna/.sqlx/query-c9e3089b1ffd52d20cfcd89e71e051c0f351643dce9be4b84b6343909c816c22.json
  12. 34 16
      apps/fortuna/.sqlx/query-f58bdd3e0ecb30f35356c22e9ab1b3802f8eebda455efabc18d30f02d23787b7.json
  13. 1 1
      apps/fortuna/Cargo.lock
  14. 1 1
      apps/fortuna/Cargo.toml
  15. 3 2
      apps/fortuna/migrations/20250502164500_init.up.sql
  16. 4 0
      apps/fortuna/migrations/20250521203448_gas.down.sql
  17. 5 0
      apps/fortuna/migrations/20250521203448_gas.up.sql
  18. 6 0
      apps/fortuna/src/api.rs
  19. 34 13
      apps/fortuna/src/api/explorer.rs
  20. 32 5
      apps/fortuna/src/chain/ethereum.rs
  21. 6 0
      apps/fortuna/src/command/run.rs
  22. 0 7
      apps/fortuna/src/config.rs
  23. 207 39
      apps/fortuna/src/history.rs
  24. 8 18
      apps/fortuna/src/keeper.rs
  25. 3 4
      apps/fortuna/src/keeper/block.rs
  26. 13 0
      apps/fortuna/src/keeper/process_event.rs
  27. 1 0
      apps/fortuna/src/lib.rs
  28. 21 0
      apps/fortuna/src/serde.rs

+ 12 - 0
apps/fortuna/.sqlx/query-03901bcfb28b127d99fe8a53e480b88336dd2aab632411114f02ce8dd8fe07e8.json

@@ -0,0 +1,12 @@
+{
+  "db_name": "SQLite",
+  "query": "UPDATE request SET state = ?, last_updated_at = ?, info = ?, provider_random_number = ? WHERE network_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ? AND state = 'Pending'",
+  "describe": {
+    "columns": [],
+    "parameters": {
+      "Right": 8
+    },
+    "nullable": []
+  },
+  "hash": "03901bcfb28b127d99fe8a53e480b88336dd2aab632411114f02ce8dd8fe07e8"
+}

+ 0 - 12
apps/fortuna/.sqlx/query-16635b3d9c6f9b743614e0e08bfa2b26d7ec6346f0323d9f16b98c32fd9a91f6.json

@@ -1,12 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "INSERT INTO request(chain_id, provider, sequence, created_at, last_updated_at, state, request_block_number, request_tx_hash, user_random_number, sender) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-  "describe": {
-    "columns": [],
-    "parameters": {
-      "Right": 10
-    },
-    "nullable": []
-  },
-  "hash": "16635b3d9c6f9b743614e0e08bfa2b26d7ec6346f0323d9f16b98c32fd9a91f6"
-}

+ 35 - 17
apps/fortuna/.sqlx/query-b848d03ffc893e1719d364beb32976ef879e79727c660c973bdad670082f5c36.json → apps/fortuna/.sqlx/query-392da9e5fdd212a4a665c86e5fc6d4f619355294490248e656ad0fc97a252471.json

@@ -1,6 +1,6 @@
 {
   "db_name": "SQLite",
-  "query": "SELECT * FROM request WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ?",
+  "query": "SELECT * FROM request WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
   "describe": {
     "columns": [
       {
@@ -9,73 +9,88 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
     "parameters": {
-      "Right": 3
+      "Right": 4
     },
     "nullable": [
       false,
@@ -88,11 +103,14 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
-  "hash": "b848d03ffc893e1719d364beb32976ef879e79727c660c973bdad670082f5c36"
+  "hash": "392da9e5fdd212a4a665c86e5fc6d4f619355294490248e656ad0fc97a252471"
 }

+ 3 - 3
apps/fortuna/.sqlx/query-9d7448c9bbad50d6242dfc0ba7d5ad4837201a1585bd56cc9a65fe75d0fa5952.json → apps/fortuna/.sqlx/query-4c8c05ec08e128d847faafdd3d79fa50da70066f30b74f354e5d3a843ba6a2c0.json

@@ -1,12 +1,12 @@
 {
   "db_name": "SQLite",
-  "query": "UPDATE request SET state = ?, last_updated_at = ?, reveal_block_number = ?, reveal_tx_hash = ?, provider_random_number = ? WHERE chain_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ?",
+  "query": "UPDATE request SET state = ?, last_updated_at = ?, reveal_block_number = ?, reveal_tx_hash = ?, provider_random_number =?, gas_used = ? WHERE network_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ?",
   "describe": {
     "columns": [],
     "parameters": {
-      "Right": 9
+      "Right": 10
     },
     "nullable": []
   },
-  "hash": "9d7448c9bbad50d6242dfc0ba7d5ad4837201a1585bd56cc9a65fe75d0fa5952"
+  "hash": "4c8c05ec08e128d847faafdd3d79fa50da70066f30b74f354e5d3a843ba6a2c0"
 }

+ 35 - 17
apps/fortuna/.sqlx/query-ba011bb5690ad6821689bec939c5303c8619b6302ef33145db3bf62259492783.json → apps/fortuna/.sqlx/query-78be8c62d5eb764995221f927b0f166e38d6fba8eb8fddb07f50c572fd27b4e2.json

@@ -1,6 +1,6 @@
 {
   "db_name": "SQLite",
-  "query": "SELECT * FROM request WHERE chain_id = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ?",
+  "query": "SELECT * FROM request WHERE network_id = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
   "describe": {
     "columns": [
       {
@@ -9,73 +9,88 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
     "parameters": {
-      "Right": 4
+      "Right": 5
     },
     "nullable": [
       false,
@@ -88,11 +103,14 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
-  "hash": "ba011bb5690ad6821689bec939c5303c8619b6302ef33145db3bf62259492783"
+  "hash": "78be8c62d5eb764995221f927b0f166e38d6fba8eb8fddb07f50c572fd27b4e2"
 }

+ 32 - 14
apps/fortuna/.sqlx/query-8cd10cd5839b81bd9538aeb10fdfd27c6e36baf5d90a4fb9e61718f021812710.json

@@ -9,68 +9,83 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
@@ -88,10 +103,13 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
   "hash": "8cd10cd5839b81bd9538aeb10fdfd27c6e36baf5d90a4fb9e61718f021812710"

+ 32 - 14
apps/fortuna/.sqlx/query-905dbc91cd5319537c5c194277d531689ac5c1338396414467496d0f50ddc3f0.json

@@ -9,68 +9,83 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
@@ -88,10 +103,13 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
   "hash": "905dbc91cd5319537c5c194277d531689ac5c1338396414467496d0f50ddc3f0"

+ 32 - 14
apps/fortuna/.sqlx/query-a62e094cee65ae58bd12ce7d3e7df44f5aca31520d1ceced83f492945e850764.json

@@ -9,68 +9,83 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
@@ -88,10 +103,13 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
   "hash": "a62e094cee65ae58bd12ce7d3e7df44f5aca31520d1ceced83f492945e850764"

+ 12 - 0
apps/fortuna/.sqlx/query-b0d9afebb3825c3509ad80e5ebab5d72360326593407518770fe537ac3da1e10.json

@@ -0,0 +1,12 @@
+{
+  "db_name": "SQLite",
+  "query": "INSERT INTO request(chain_id, network_id, provider, sequence, created_at, last_updated_at, state, request_block_number, request_tx_hash, user_random_number, sender, gas_limit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+  "describe": {
+    "columns": [],
+    "parameters": {
+      "Right": 12
+    },
+    "nullable": []
+  },
+  "hash": "b0d9afebb3825c3509ad80e5ebab5d72360326593407518770fe537ac3da1e10"
+}

+ 0 - 12
apps/fortuna/.sqlx/query-b2baa9f9d46f873a3a7117c38ecab09f56082c5267dbf5180f39c608b6262f5a.json

@@ -1,12 +0,0 @@
-{
-  "db_name": "SQLite",
-  "query": "UPDATE request SET state = ?, last_updated_at = ?, info = ? WHERE chain_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ? AND state = 'Pending'",
-  "describe": {
-    "columns": [],
-    "parameters": {
-      "Right": 7
-    },
-    "nullable": []
-  },
-  "hash": "b2baa9f9d46f873a3a7117c38ecab09f56082c5267dbf5180f39c608b6262f5a"
-}

+ 34 - 16
apps/fortuna/.sqlx/query-795b81369e5b039cfa38df06bd6c8da8610d84f19a294fb8d3a8370a47a3f241.json → apps/fortuna/.sqlx/query-c9e3089b1ffd52d20cfcd89e71e051c0f351643dce9be4b84b6343909c816c22.json

@@ -1,6 +1,6 @@
 {
   "db_name": "SQLite",
-  "query": "SELECT * FROM request WHERE sender = ? AND chain_id = ?",
+  "query": "SELECT * FROM request WHERE sequence = ? AND network_id = ?",
   "describe": {
     "columns": [
       {
@@ -9,68 +9,83 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
@@ -88,11 +103,14 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
-  "hash": "795b81369e5b039cfa38df06bd6c8da8610d84f19a294fb8d3a8370a47a3f241"
+  "hash": "c9e3089b1ffd52d20cfcd89e71e051c0f351643dce9be4b84b6343909c816c22"
 }

+ 34 - 16
apps/fortuna/.sqlx/query-7d4365a9cb7c9ec16fd4ca60e1d558419954a0326b29180fa9943605813f04e6.json → apps/fortuna/.sqlx/query-f58bdd3e0ecb30f35356c22e9ab1b3802f8eebda455efabc18d30f02d23787b7.json

@@ -1,6 +1,6 @@
 {
   "db_name": "SQLite",
-  "query": "SELECT * FROM request WHERE sequence = ? AND chain_id = ?",
+  "query": "SELECT * FROM request WHERE sender = ? AND network_id = ?",
   "describe": {
     "columns": [
       {
@@ -9,68 +9,83 @@
         "type_info": "Text"
       },
       {
-        "name": "provider",
+        "name": "network_id",
         "ordinal": 1,
+        "type_info": "Integer"
+      },
+      {
+        "name": "provider",
+        "ordinal": 2,
         "type_info": "Text"
       },
       {
         "name": "sequence",
-        "ordinal": 2,
+        "ordinal": 3,
         "type_info": "Integer"
       },
       {
         "name": "created_at",
-        "ordinal": 3,
+        "ordinal": 4,
         "type_info": "Datetime"
       },
       {
         "name": "last_updated_at",
-        "ordinal": 4,
+        "ordinal": 5,
         "type_info": "Datetime"
       },
       {
         "name": "state",
-        "ordinal": 5,
+        "ordinal": 6,
         "type_info": "Text"
       },
       {
         "name": "request_block_number",
-        "ordinal": 6,
+        "ordinal": 7,
         "type_info": "Integer"
       },
       {
         "name": "request_tx_hash",
-        "ordinal": 7,
+        "ordinal": 8,
         "type_info": "Text"
       },
       {
         "name": "user_random_number",
-        "ordinal": 8,
+        "ordinal": 9,
         "type_info": "Text"
       },
       {
         "name": "sender",
-        "ordinal": 9,
+        "ordinal": 10,
         "type_info": "Text"
       },
       {
         "name": "reveal_block_number",
-        "ordinal": 10,
+        "ordinal": 11,
         "type_info": "Integer"
       },
       {
         "name": "reveal_tx_hash",
-        "ordinal": 11,
+        "ordinal": 12,
         "type_info": "Text"
       },
       {
         "name": "provider_random_number",
-        "ordinal": 12,
+        "ordinal": 13,
         "type_info": "Text"
       },
       {
         "name": "info",
-        "ordinal": 13,
+        "ordinal": 14,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_used",
+        "ordinal": 15,
+        "type_info": "Text"
+      },
+      {
+        "name": "gas_limit",
+        "ordinal": 16,
         "type_info": "Text"
       }
     ],
@@ -88,11 +103,14 @@
       false,
       false,
       false,
+      false,
+      true,
+      true,
       true,
       true,
       true,
-      true
+      false
     ]
   },
-  "hash": "7d4365a9cb7c9ec16fd4ca60e1d558419954a0326b29180fa9943605813f04e6"
+  "hash": "f58bdd3e0ecb30f35356c22e9ab1b3802f8eebda455efabc18d30f02d23787b7"
 }

+ 1 - 1
apps/fortuna/Cargo.lock

@@ -1647,7 +1647,7 @@ dependencies = [
 
 [[package]]
 name = "fortuna"
-version = "7.6.1"
+version = "7.6.2"
 dependencies = [
  "anyhow",
  "axum",

+ 1 - 1
apps/fortuna/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "fortuna"
-version = "7.6.1"
+version = "7.6.2"
 edition = "2021"
 
 [lib]

+ 3 - 2
apps/fortuna/migrations/20250502164500_init.up.sql

@@ -1,6 +1,7 @@
 -- we use VARCHAR(40) for addresses and VARCHAR(64) for tx_hashes and 32 byte numbers
 CREATE TABLE request(
                     chain_id VARCHAR(20) NOT NULL,
+                    network_id INTEGER NOT NULL,
                     provider VARCHAR(40) NOT NULL,
                     sequence INTEGER NOT NULL,
                     created_at DATETIME NOT NULL,
@@ -14,11 +15,11 @@ CREATE TABLE request(
                     reveal_tx_hash VARCHAR(64),
                     provider_random_number VARCHAR(64),
                     info TEXT,
-                    PRIMARY KEY (chain_id, sequence, provider, request_tx_hash)
+                    PRIMARY KEY (network_id, sequence, provider, request_tx_hash)
 );
 
 CREATE INDEX idx_request_sequence ON request (sequence);
-CREATE INDEX idx_request_chain_id_created_at ON request (chain_id, created_at);
+CREATE INDEX idx_request_network_id_created_at ON request (network_id, created_at);
 CREATE INDEX idx_request_created_at ON request (created_at);
 CREATE INDEX idx_request_request_tx_hash ON request (request_tx_hash) WHERE request_tx_hash IS NOT NULL;
 CREATE INDEX idx_request_reveal_tx_hash ON request (reveal_tx_hash) WHERE reveal_tx_hash IS NOT NULL;

+ 4 - 0
apps/fortuna/migrations/20250521203448_gas.down.sql

@@ -0,0 +1,4 @@
+ALTER TABLE request
+DROP COLUMN gas_used;
+ALTER TABLE request
+DROP COLUMN gas_limit;

+ 5 - 0
apps/fortuna/migrations/20250521203448_gas.up.sql

@@ -0,0 +1,5 @@
+-- U256 max value is 78 digits, so 100 is a safe upper bound
+ALTER TABLE request
+ADD COLUMN gas_used VARCHAR(100);
+ALTER TABLE request
+ADD COLUMN gas_limit VARCHAR(100) NOT NULL;

+ 6 - 0
apps/fortuna/src/api.rs

@@ -33,6 +33,7 @@ mod ready;
 mod revelation;
 
 pub type ChainId = String;
+pub type NetworkId = u64;
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
 pub struct RequestLabel {
@@ -86,6 +87,9 @@ impl ApiState {
 pub struct BlockchainState {
     /// The chain id for this blockchain, useful for logging
     pub id: ChainId,
+    /// The network id for this blockchain
+    /// Obtained from the response of eth_chainId rpc call
+    pub network_id: u64,
     /// The hash chain(s) required to serve random numbers for this blockchain
     pub state: Arc<HashChainState>,
     /// The contract that the server is fulfilling requests for.
@@ -239,6 +243,7 @@ mod test {
 
         let eth_state = BlockchainState {
             id: "ethereum".into(),
+            network_id: 1,
             state: ETH_CHAIN.clone(),
             contract: eth_read.clone(),
             provider_address: PROVIDER,
@@ -252,6 +257,7 @@ mod test {
 
         let avax_state = BlockchainState {
             id: "avalanche".into(),
+            network_id: 43114,
             state: AVAX_CHAIN.clone(),
             contract: avax_read.clone(),
             provider_address: PROVIDER,

+ 34 - 13
apps/fortuna/src/api/explorer.rs

@@ -1,6 +1,6 @@
 use {
     crate::{
-        api::{ChainId, RestError},
+        api::{ApiBlockChainState, NetworkId, RestError},
         history::RequestStatus,
     },
     axum::{
@@ -16,24 +16,30 @@ use {
 #[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)]
 #[into_params(parameter_in=Query)]
 pub struct ExplorerQueryParams {
-    /// Only return logs that are newer or equal to this timestamp.
+    /// Only return logs that are newer or equal to this timestamp. Timestamp is in ISO 8601 format with UTC timezone.
     #[param(value_type = Option<String>, example = "2023-10-01T00:00:00Z")]
     pub min_timestamp: Option<DateTime<Utc>>,
-    /// Only return logs that are older or equal to this timestamp.
+    /// Only return logs that are older or equal to this timestamp. Timestamp is in ISO 8601 format with UTC timezone.
     #[param(value_type = Option<String>, example = "2033-10-01T00:00:00Z")]
     pub max_timestamp: Option<DateTime<Utc>>,
     /// The query string to search for. This can be a transaction hash, sender address, or sequence number.
     pub query: Option<String>,
-    #[param(value_type = Option<String>)]
-    /// The chain ID to filter the results by.
-    pub chain_id: Option<ChainId>,
+    /// The network ID to filter the results by.
+    #[param(value_type = Option<u64>)]
+    pub network_id: Option<NetworkId>,
+    /// The maximum number of logs to return. Max value is 1000.
+    #[param(default = 1000)]
+    pub limit: Option<u64>,
+    /// The offset to start returning logs from.
+    #[param(default = 0)]
+    pub offset: Option<u64>,
 }
 
 const LOG_RETURN_LIMIT: u64 = 1000;
 
 /// Returns the logs of all requests captured by the keeper.
 ///
-/// This endpoint allows you to filter the logs by a specific chain ID, a query string (which can be a transaction hash, sender address, or sequence number), and a time range.
+/// This endpoint allows you to filter the logs by a specific network ID, a query string (which can be a transaction hash, sender address, or sequence number), and a time range.
 /// This is useful for debugging and monitoring the requests made to the Entropy contracts on various chains.
 #[utoipa::path(
     get,
@@ -45,11 +51,25 @@ pub async fn explorer(
     State(state): State<crate::api::ApiState>,
     Query(query_params): Query<ExplorerQueryParams>,
 ) -> anyhow::Result<Json<Vec<RequestStatus>>, RestError> {
-    if let Some(chain_id) = &query_params.chain_id {
-        if !state.chains.read().await.contains_key(chain_id) {
+    if let Some(network_id) = &query_params.network_id {
+        if !state
+            .chains
+            .read()
+            .await
+            .iter()
+            .any(|(_, state)| match state {
+                ApiBlockChainState::Uninitialized => false,
+                ApiBlockChainState::Initialized(state) => state.network_id == *network_id,
+            })
+        {
             return Err(RestError::InvalidChainId);
         }
     }
+    if let Some(limit) = query_params.limit {
+        if limit > LOG_RETURN_LIMIT || limit == 0 {
+            return Err(RestError::InvalidQueryString);
+        }
+    }
     if let Some(query) = query_params.query {
         if let Ok(tx_hash) = TxHash::from_str(&query) {
             return Ok(Json(
@@ -64,7 +84,7 @@ pub async fn explorer(
             return Ok(Json(
                 state
                     .history
-                    .get_requests_by_sender(sender, query_params.chain_id)
+                    .get_requests_by_sender(sender, query_params.network_id)
                     .await
                     .map_err(|_| RestError::TemporarilyUnavailable)?,
             ));
@@ -73,7 +93,7 @@ pub async fn explorer(
             return Ok(Json(
                 state
                     .history
-                    .get_requests_by_sequence(sequence_number, query_params.chain_id)
+                    .get_requests_by_sequence(sequence_number, query_params.network_id)
                     .await
                     .map_err(|_| RestError::TemporarilyUnavailable)?,
             ));
@@ -84,8 +104,9 @@ pub async fn explorer(
         state
             .history
             .get_requests_by_time(
-                query_params.chain_id,
-                LOG_RETURN_LIMIT,
+                query_params.network_id,
+                query_params.limit.unwrap_or(LOG_RETURN_LIMIT),
+                query_params.offset.unwrap_or(0),
                 query_params.min_timestamp,
                 query_params.max_timestamp,
             )

+ 32 - 5
apps/fortuna/src/chain/ethereum.rs

@@ -160,17 +160,17 @@ impl<T: JsonRpcClient + 'static + Clone> SignablePythContractInner<T> {
         }
     }
 
-    pub async fn from_config_and_provider(
+    pub fn from_config_and_provider_and_network_id(
         chain_config: &EthereumConfig,
         private_key: &str,
         provider: Provider<T>,
+        network_id: u64,
     ) -> Result<SignablePythContractInner<T>> {
-        let chain_id = provider.get_chainid().await?;
         let gas_oracle =
             EthProviderOracle::new(provider.clone(), chain_config.priority_fee_multiplier_pct);
         let wallet__ = private_key
             .parse::<LocalWallet>()?
-            .with_chain_id(chain_id.as_u64());
+            .with_chain_id(network_id);
 
         let address = wallet__.address();
 
@@ -185,6 +185,20 @@ impl<T: JsonRpcClient + 'static + Clone> SignablePythContractInner<T> {
             )),
         ))
     }
+
+    pub async fn from_config_and_provider(
+        chain_config: &EthereumConfig,
+        private_key: &str,
+        provider: Provider<T>,
+    ) -> Result<SignablePythContractInner<T>> {
+        let network_id = provider.get_chainid().await?.as_u64();
+        Self::from_config_and_provider_and_network_id(
+            chain_config,
+            private_key,
+            provider,
+            network_id,
+        )
+    }
 }
 
 impl SignablePythContract {
@@ -195,14 +209,20 @@ impl SignablePythContract {
 }
 
 impl InstrumentedSignablePythContract {
-    pub async fn from_config(
+    pub fn from_config(
         chain_config: &EthereumConfig,
         private_key: &str,
         chain_id: ChainId,
         metrics: Arc<RpcMetrics>,
+        network_id: u64,
     ) -> Result<Self> {
         let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?;
-        Self::from_config_and_provider(chain_config, private_key, provider).await
+        Self::from_config_and_provider_and_network_id(
+            chain_config,
+            private_key,
+            provider,
+            network_id,
+        )
     }
 }
 
@@ -232,6 +252,13 @@ impl InstrumentedPythContract {
     }
 }
 
+impl<T: JsonRpcClient + 'static> PythRandom<Provider<T>> {
+    pub async fn get_network_id(&self) -> Result<U256> {
+        let chain_id = self.client().get_chainid().await?;
+        Ok(chain_id)
+    }
+}
+
 #[async_trait]
 impl<T: JsonRpcClient + 'static> EntropyReader for PythRandom<Provider<T>> {
     async fn get_request(

+ 6 - 0
apps/fortuna/src/command/run.rs

@@ -216,6 +216,11 @@ async fn setup_chain_state(
         chain_id.clone(),
         rpc_metrics,
     )?);
+    let network_id: u64 = contract
+        .get_network_id()
+        .await
+        .map_err(|e| anyhow!("Failed to get network id: {}. Chain id: {}", &chain_id, e))?
+        .as_u64();
     let mut provider_commitments = chain_config.commitments.clone().unwrap_or_default();
     provider_commitments.sort_by(|c1, c2| {
         c1.original_commitment_sequence_number
@@ -295,6 +300,7 @@ async fn setup_chain_state(
     let state = BlockchainState {
         id: chain_id.clone(),
         state: Arc::new(chain_state),
+        network_id,
         contract,
         provider_address: *provider,
         reveal_delay_blocks: chain_config.reveal_delay_blocks,

+ 0 - 7
apps/fortuna/src/config.rs

@@ -189,9 +189,6 @@ pub struct EthereumConfig {
     /// at each specified delay. For example: [5, 10, 20].
     #[serde(default = "default_block_delays")]
     pub block_delays: Vec<u64>,
-
-    #[serde(default = "default_retry_previous_blocks")]
-    pub retry_previous_blocks: u64,
 }
 
 fn default_sync_fee_only_on_register() -> bool {
@@ -202,10 +199,6 @@ fn default_block_delays() -> Vec<u64> {
     vec![5]
 }
 
-fn default_retry_previous_blocks() -> u64 {
-    100
-}
-
 fn default_priority_fee_multiplier_pct() -> u64 {
     100
 }

+ 207 - 39
apps/fortuna/src/history.rs

@@ -1,8 +1,13 @@
 use {
-    crate::api::ChainId,
+    crate::api::{ChainId, NetworkId},
     anyhow::Result,
     chrono::{DateTime, NaiveDateTime},
-    ethers::{core::utils::hex::ToHex, prelude::TxHash, types::Address},
+    ethers::{
+        core::utils::hex::ToHex,
+        prelude::TxHash,
+        types::{Address, U256},
+        utils::keccak256,
+    },
     serde::Serialize,
     serde_with::serde_as,
     sqlx::{migrate, Pool, Sqlite, SqlitePool},
@@ -17,6 +22,7 @@ use {
 pub enum RequestEntryState {
     Pending,
     Completed {
+        /// The block number of the reveal transaction.
         reveal_block_number: u64,
         /// The transaction hash of the reveal transaction.
         #[schema(example = "0xfe5f880ac10c0aae43f910b5a17f98a93cdd2eb2dce3a5ae34e5827a3a071a32", value_type = String)]
@@ -25,9 +31,22 @@ pub enum RequestEntryState {
         #[schema(example = "a905ab56567d31a7fda38ed819d97bc257f3ebe385fc5c72ce226d3bb855f0fe")]
         #[serde_as(as = "serde_with::hex::Hex")]
         provider_random_number: [u8; 32],
+        /// The gas used for the reveal transaction in the smallest unit of the chain.
+        /// For example, if the native currency is ETH, this will be in wei.
+        #[schema(example = "567890", value_type = String)]
+        #[serde(with = "crate::serde::u256")]
+        gas_used: U256,
+        /// The combined random number generated from the user and provider contributions.
+        #[schema(example = "a905ab56567d31a7fda38ed819d97bc257f3ebe385fc5c72ce226d3bb855f0fe")]
+        #[serde_as(as = "serde_with::hex::Hex")]
+        combined_random_number: [u8; 32],
     },
     Failed {
         reason: String,
+        /// The provider contribution to the random number.
+        #[schema(example = "a905ab56567d31a7fda38ed819d97bc257f3ebe385fc5c72ce226d3bb855f0fe")]
+        #[serde_as(as = "Option<serde_with::hex::Hex>")]
+        provider_random_number: Option<[u8; 32]>,
     },
 }
 
@@ -37,6 +56,9 @@ pub struct RequestStatus {
     /// The chain ID of the request.
     #[schema(example = "ethereum", value_type = String)]
     pub chain_id: ChainId,
+    /// The network ID of the request. This is the response of eth_chainId rpc call.
+    #[schema(example = "1", value_type = u64)]
+    pub network_id: NetworkId,
     #[schema(example = "0x6cc14824ea2918f5de5c2f75a9da968ad4bd6344", value_type = String)]
     pub provider: Address,
     pub sequence: u64,
@@ -48,6 +70,11 @@ pub struct RequestStatus {
     /// The transaction hash of the request transaction.
     #[schema(example = "0x5a3a984f41bb5443f5efa6070ed59ccb25edd8dbe6ce7f9294cf5caa64ed00ae", value_type = String)]
     pub request_tx_hash: TxHash,
+    /// Gas limit for the callback in the smallest unit of the chain.
+    /// For example, if the native currency is ETH, this will be in wei.
+    #[schema(example = "500000", value_type = String)]
+    #[serde(with = "crate::serde::u256")]
+    pub gas_limit: U256,
     /// The user contribution to the random number.
     #[schema(example = "a905ab56567d31a7fda38ed819d97bc257f3ebe385fc5c72ce226d3bb855f0fe")]
     #[serde_as(as = "serde_with::hex::Hex")]
@@ -58,9 +85,22 @@ pub struct RequestStatus {
     pub state: RequestEntryState,
 }
 
+impl RequestStatus {
+    pub fn generate_combined_random_number(
+        user_random_number: &[u8; 32],
+        provider_random_number: &[u8; 32],
+    ) -> [u8; 32] {
+        let mut concat: [u8; 96] = [0; 96]; // last 32 bytes are for the block hash which is not used here
+        concat[0..32].copy_from_slice(user_random_number);
+        concat[32..64].copy_from_slice(provider_random_number);
+        keccak256(concat)
+    }
+}
+
 #[derive(Clone, Debug, Serialize, ToSchema, PartialEq)]
 struct RequestRow {
     chain_id: String,
+    network_id: i64,
     provider: String,
     sequence: i64,
     created_at: NaiveDateTime,
@@ -70,9 +110,11 @@ struct RequestRow {
     request_tx_hash: String,
     user_random_number: String,
     sender: String,
+    gas_limit: String,
     reveal_block_number: Option<i64>,
     reveal_tx_hash: Option<String>,
     provider_random_number: Option<String>,
+    gas_used: Option<String>,
     info: Option<String>,
 }
 
@@ -81,6 +123,7 @@ impl TryFrom<RequestRow> for RequestStatus {
 
     fn try_from(row: RequestRow) -> Result<Self, Self::Error> {
         let chain_id = row.chain_id;
+        let network_id = row.network_id as u64;
         let provider = row.provider.parse()?;
         let sequence = row.sequence as u64;
         let created_at = row.created_at.and_utc();
@@ -89,6 +132,8 @@ impl TryFrom<RequestRow> for RequestStatus {
         let user_random_number = hex::FromHex::from_hex(row.user_random_number)?;
         let request_tx_hash = row.request_tx_hash.parse()?;
         let sender = row.sender.parse()?;
+        let gas_limit = U256::from_dec_str(&row.gas_limit)
+            .map_err(|_| anyhow::anyhow!("Failed to parse gas limit"))?;
 
         let state = match row.state.as_str() {
             "Pending" => RequestEntryState::Pending,
@@ -107,19 +152,36 @@ impl TryFrom<RequestRow> for RequestStatus {
                 ))?;
                 let provider_random_number: [u8; 32] =
                     hex::FromHex::from_hex(provider_random_number)?;
+                let gas_used = row
+                    .gas_used
+                    .ok_or(anyhow::anyhow!("Gas used is missing for completed request"))?;
+                let gas_used = U256::from_dec_str(&gas_used)
+                    .map_err(|_| anyhow::anyhow!("Failed to parse gas used"))?;
                 RequestEntryState::Completed {
                     reveal_block_number,
                     reveal_tx_hash,
                     provider_random_number,
+                    gas_used,
+                    combined_random_number: Self::generate_combined_random_number(
+                        &user_random_number,
+                        &provider_random_number,
+                    ),
                 }
             }
             "Failed" => RequestEntryState::Failed {
                 reason: row.info.unwrap_or_default(),
+                provider_random_number: match row.provider_random_number {
+                    Some(provider_random_number) => {
+                        Some(hex::FromHex::from_hex(provider_random_number)?)
+                    }
+                    None => None,
+                },
             },
             _ => return Err(anyhow::anyhow!("Unknown request state: {}", row.state)),
         };
         Ok(Self {
             chain_id,
+            network_id,
             provider,
             sequence,
             created_at,
@@ -129,6 +191,7 @@ impl TryFrom<RequestRow> for RequestStatus {
             request_tx_hash,
             user_random_number,
             sender,
+            gas_limit,
         })
     }
 }
@@ -185,15 +248,18 @@ impl History {
     async fn update_request_status(pool: &Pool<Sqlite>, new_status: RequestStatus) {
         let sequence = new_status.sequence as i64;
         let chain_id = new_status.chain_id;
+        let network_id = new_status.network_id as i64;
         let request_tx_hash: String = new_status.request_tx_hash.encode_hex();
         let provider: String = new_status.provider.encode_hex();
+        let gas_limit = new_status.gas_limit.to_string();
         let result = match new_status.state {
             RequestEntryState::Pending => {
                 let block_number = new_status.request_block_number as i64;
                 let sender: String = new_status.sender.encode_hex();
                 let user_random_number: String = new_status.user_random_number.encode_hex();
-                sqlx::query!("INSERT INTO request(chain_id, provider, sequence, created_at, last_updated_at, state, request_block_number, request_tx_hash, user_random_number, sender) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+                sqlx::query!("INSERT INTO request(chain_id, network_id, provider, sequence, created_at, last_updated_at, state, request_block_number, request_tx_hash, user_random_number, sender, gas_limit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                     chain_id,
+                    network_id,
                     provider,
                     sequence,
                     new_status.created_at,
@@ -202,7 +268,9 @@ impl History {
                     block_number,
                     request_tx_hash,
                     user_random_number,
-                    sender)
+                    sender,
+                    gas_limit
+            )
                     .execute(pool)
                     .await
             }
@@ -210,29 +278,45 @@ impl History {
                 reveal_block_number,
                 reveal_tx_hash,
                 provider_random_number,
+                gas_used,
+                combined_random_number: _,
             } => {
                 let reveal_block_number = reveal_block_number as i64;
                 let reveal_tx_hash: String = reveal_tx_hash.encode_hex();
                 let provider_random_number: String = provider_random_number.encode_hex();
-                sqlx::query!("UPDATE request SET state = ?, last_updated_at = ?, reveal_block_number = ?, reveal_tx_hash = ?, provider_random_number = ? WHERE chain_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ?",
+                let gas_used: String = gas_used.to_string();
+                let result = sqlx::query!("UPDATE request SET state = ?, last_updated_at = ?, reveal_block_number = ?, reveal_tx_hash = ?, provider_random_number =?, gas_used = ? WHERE network_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ?",
                     "Completed",
                     new_status.last_updated_at,
                     reveal_block_number,
                     reveal_tx_hash,
                     provider_random_number,
-                    chain_id,
+                    gas_used,
+                    network_id,
                     sequence,
                     provider,
                     request_tx_hash)
                     .execute(pool)
-                    .await
+                    .await;
+                if let Ok(query_result) = &result {
+                    if query_result.rows_affected() == 0 {
+                        tracing::error!("Failed to update request status to complete: No rows affected. Chain ID: {}, Sequence: {}, Request TX Hash: {}", network_id, sequence, request_tx_hash);
+                    }
+                }
+                result
             }
-            RequestEntryState::Failed { reason } => {
-                sqlx::query!("UPDATE request SET state = ?, last_updated_at = ?, info = ? WHERE chain_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ? AND state = 'Pending'",
+            RequestEntryState::Failed {
+                reason,
+                provider_random_number,
+            } => {
+                let provider_random_number: Option<String> = provider_random_number
+                    .map(|provider_random_number| provider_random_number.encode_hex());
+                sqlx::query!("UPDATE request SET state = ?, last_updated_at = ?, info = ?, provider_random_number = ? WHERE network_id = ? AND sequence = ? AND provider = ? AND request_tx_hash = ? AND state = 'Pending'",
                     "Failed",
                     new_status.last_updated_at,
                     reason,
-                    chain_id,
+                    provider_random_number,
+                    network_id,
                     sequence,
                     provider,
                     request_tx_hash)
@@ -271,16 +355,17 @@ impl History {
     pub async fn get_requests_by_sender(
         &self,
         sender: Address,
-        chain_id: Option<ChainId>,
+        network_id: Option<NetworkId>,
     ) -> Result<Vec<RequestStatus>> {
         let sender: String = sender.encode_hex();
-        let rows = match chain_id {
-            Some(chain_id) => {
+        let rows = match network_id {
+            Some(network_id) => {
+                let network_id = network_id as i64;
                 sqlx::query_as!(
                     RequestRow,
-                    "SELECT * FROM request WHERE sender = ? AND chain_id = ?",
+                    "SELECT * FROM request WHERE sender = ? AND network_id = ?",
                     sender,
-                    chain_id,
+                    network_id,
                 )
                 .fetch_all(&self.pool)
                 .await
@@ -301,16 +386,17 @@ impl History {
     pub async fn get_requests_by_sequence(
         &self,
         sequence: u64,
-        chain_id: Option<ChainId>,
+        network_id: Option<NetworkId>,
     ) -> Result<Vec<RequestStatus>> {
         let sequence = sequence as i64;
-        let rows = match chain_id {
-            Some(chain_id) => {
+        let rows = match network_id {
+            Some(network_id) => {
+                let network_id = network_id as i64;
                 sqlx::query_as!(
                     RequestRow,
-                    "SELECT * FROM request WHERE sequence = ? AND chain_id = ?",
+                    "SELECT * FROM request WHERE sequence = ? AND network_id = ?",
                     sequence,
-                    chain_id,
+                    network_id,
                 )
                 .fetch_all(&self.pool)
                 .await
@@ -334,8 +420,9 @@ impl History {
 
     pub async fn get_requests_by_time(
         &self,
-        chain_id: Option<ChainId>,
+        network_id: Option<NetworkId>,
         limit: u64,
+        offset: u64,
         min_timestamp: Option<DateTime<chrono::Utc>>,
         max_timestamp: Option<DateTime<chrono::Utc>>,
     ) -> Result<Vec<RequestStatus>> {
@@ -352,20 +439,23 @@ impl History {
                 .unwrap(),
         );
         let limit = limit as i64;
-        let rows = match chain_id {
-            Some(chain_id) => {
-                let chain_id = chain_id.to_string();
-                sqlx::query_as!(RequestRow, "SELECT * FROM request WHERE chain_id = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ?",
-                    chain_id,
+        let offset = offset as i64;
+        let rows = match network_id {
+            Some(network_id) => {
+                let network_id = network_id as i64;
+                sqlx::query_as!(RequestRow, "SELECT * FROM request WHERE network_id = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
+                    network_id,
                     min_timestamp,
                     max_timestamp,
-                    limit).fetch_all(&self.pool).await
+                    limit,
+                offset).fetch_all(&self.pool).await
             }
             None => {
-                sqlx::query_as!(RequestRow, "SELECT * FROM request WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ?",
+                sqlx::query_as!(RequestRow, "SELECT * FROM request WHERE created_at >= ? AND created_at <= ? ORDER BY created_at DESC LIMIT ? OFFSET ?",
                     min_timestamp,
                     max_timestamp,
-                    limit).fetch_all(&self.pool).await
+                    limit,
+                offset).fetch_all(&self.pool).await
             }
         }.map_err(|e| {
             tracing::error!("Failed to fetch request by time: {}", e);
@@ -382,6 +472,7 @@ mod test {
     fn get_random_request_status() -> RequestStatus {
         RequestStatus {
             chain_id: "ethereum".to_string(),
+            network_id: 121,
             provider: Address::random(),
             sequence: 1,
             created_at: chrono::Utc::now(),
@@ -391,6 +482,7 @@ mod test {
             user_random_number: [20; 32],
             sender: Address::random(),
             state: RequestEntryState::Pending,
+            gas_limit: U256::from(500_000),
         }
     }
 
@@ -404,11 +496,16 @@ mod test {
             reveal_block_number: 1,
             reveal_tx_hash,
             provider_random_number: [40; 32],
+            gas_used: U256::from(567890),
+            combined_random_number: RequestStatus::generate_combined_random_number(
+                &status.user_random_number,
+                &[40; 32],
+            ),
         };
         History::update_request_status(&history.pool, status.clone()).await;
 
         let logs = history
-            .get_requests_by_sequence(status.sequence, Some(status.chain_id.clone()))
+            .get_requests_by_sequence(status.sequence, Some(status.network_id))
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
@@ -432,7 +529,7 @@ mod test {
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_sender(status.sender, Some(status.chain_id.clone()))
+            .get_requests_by_sender(status.sender, Some(status.network_id))
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
@@ -445,14 +542,82 @@ mod test {
     }
 
     #[tokio::test]
+    async fn test_no_transition_from_completed_to_failed() {
+        let history = History::new_in_memory().await.unwrap();
+        let reveal_tx_hash = TxHash::random();
+        let mut status = get_random_request_status();
+        History::update_request_status(&history.pool, status.clone()).await;
+        status.state = RequestEntryState::Completed {
+            reveal_block_number: 1,
+            reveal_tx_hash,
+            provider_random_number: [40; 32],
+            gas_used: U256::from(567890),
+            combined_random_number: RequestStatus::generate_combined_random_number(
+                &status.user_random_number,
+                &[40; 32],
+            ),
+        };
+        History::update_request_status(&history.pool, status.clone()).await;
+        let mut failed_status = status.clone();
+        failed_status.state = RequestEntryState::Failed {
+            reason: "Failed".to_string(),
+            provider_random_number: None,
+        };
+        History::update_request_status(&history.pool, failed_status).await;
+
+        let logs = history
+            .get_requests_by_tx_hash(reveal_tx_hash)
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+    }
+
+    #[tokio::test]
+    async fn test_failed_state() {
+        let history = History::new_in_memory().await.unwrap();
+        let mut status = get_random_request_status();
+        History::update_request_status(&history.pool, status.clone()).await;
+        status.state = RequestEntryState::Failed {
+            reason: "Failed".to_string(),
+            provider_random_number: Some([40; 32]),
+        };
+        History::update_request_status(&history.pool, status.clone()).await;
+        let logs = history
+            .get_requests_by_tx_hash(status.request_tx_hash)
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+    }
+
+    #[tokio::test]
+    async fn test_generate_combined_random_number() {
+        let user_random_number = hex::FromHex::from_hex(
+            "0000000000000000000000006c8ac03d388d5572f77aca84573628ee87a7a4da",
+        )
+        .unwrap();
+        let provider_random_number = hex::FromHex::from_hex(
+            "deeb67cb894c33f7b20ae484228a9096b51e8db11461fcb0975c681cf0875d37",
+        )
+        .unwrap();
+        let combined_random_number = RequestStatus::generate_combined_random_number(
+            &user_random_number,
+            &provider_random_number,
+        );
+        let expected_combined_random_number: [u8; 32] = hex::FromHex::from_hex(
+            "1c26ffa1f8430dc91cb755a98bf37ce82ac0e2cfd961e10111935917694609d5",
+        )
+        .unwrap();
+        assert_eq!(combined_random_number, expected_combined_random_number,);
+    }
 
+    #[tokio::test]
     async fn test_history_filter_irrelevant_logs() {
         let history = History::new_in_memory().await.unwrap();
         let status = get_random_request_status();
         History::update_request_status(&history.pool, status.clone()).await;
 
         let logs = history
-            .get_requests_by_sequence(status.sequence, Some("not-ethereum".to_string()))
+            .get_requests_by_sequence(status.sequence, Some(123))
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
@@ -470,7 +635,7 @@ mod test {
         assert_eq!(logs, vec![]);
 
         let logs = history
-            .get_requests_by_sender(Address::zero(), Some(status.chain_id.clone()))
+            .get_requests_by_sender(Address::zero(), Some(status.network_id))
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
@@ -487,12 +652,13 @@ mod test {
         let history = History::new_in_memory().await.unwrap();
         let status = get_random_request_status();
         History::update_request_status(&history.pool, status.clone()).await;
-        for chain_id in [None, Some("ethereum".to_string())] {
+        for network_id in [None, Some(121)] {
             // min = created_at = max
             let logs = history
                 .get_requests_by_time(
-                    chain_id.clone(),
+                    network_id,
                     10,
+                    0,
                     Some(status.created_at),
                     Some(status.created_at),
                 )
@@ -503,8 +669,9 @@ mod test {
             // min = created_at + 1
             let logs = history
                 .get_requests_by_time(
-                    chain_id.clone(),
+                    network_id,
                     10,
+                    0,
                     Some(status.created_at + Duration::seconds(1)),
                     None,
                 )
@@ -515,8 +682,9 @@ mod test {
             // max = created_at - 1
             let logs = history
                 .get_requests_by_time(
-                    chain_id.clone(),
+                    network_id,
                     10,
+                    0,
                     None,
                     Some(status.created_at - Duration::seconds(1)),
                 )
@@ -526,7 +694,7 @@ mod test {
 
             // no min or max
             let logs = history
-                .get_requests_by_time(chain_id.clone(), 10, None, None)
+                .get_requests_by_time(network_id, 10, 0, None, None)
                 .await
                 .unwrap();
             assert_eq!(logs, vec![status.clone()]);
@@ -541,7 +709,7 @@ mod test {
         // wait for the writer thread to write to the db
         sleep(std::time::Duration::from_secs(1)).await;
         let logs = history
-            .get_requests_by_sequence(1, Some("ethereum".to_string()))
+            .get_requests_by_sequence(1, Some(121))
             .await
             .unwrap();
         assert_eq!(logs, vec![status]);

+ 8 - 18
apps/fortuna/src/keeper.rs

@@ -67,15 +67,13 @@ pub async fn run_keeper_threads(
     let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
     tracing::info!("Latest safe block: {}", &latest_safe_block);
 
-    let contract = Arc::new(
-        InstrumentedSignablePythContract::from_config(
-            &chain_eth_config,
-            &private_key,
-            chain_state.id.clone(),
-            rpc_metrics.clone(),
-        )
-        .await?,
-    );
+    let contract = Arc::new(InstrumentedSignablePythContract::from_config(
+        &chain_eth_config,
+        &private_key,
+        chain_state.id.clone(),
+        rpc_metrics.clone(),
+        chain_state.network_id,
+    )?);
     let keeper_address = contract.wallet().address();
 
     let fulfilled_requests_cache = Arc::new(RwLock::new(HashSet::<u64>::new()));
@@ -105,15 +103,7 @@ pub async fn run_keeper_threads(
 
     let (tx, rx) = mpsc::channel::<BlockRange>(1000);
     // Spawn a thread to watch for new blocks and send the range of blocks for which events has not been handled to the `tx` channel.
-    spawn(
-        watch_blocks_wrapper(
-            chain_state.clone(),
-            latest_safe_block,
-            tx,
-            chain_eth_config.retry_previous_blocks,
-        )
-        .in_current_span(),
-    );
+    spawn(watch_blocks_wrapper(chain_state.clone(), latest_safe_block, tx).in_current_span());
 
     // Spawn a thread for block processing with configured delays
     spawn(

+ 3 - 4
apps/fortuna/src/keeper/block.rs

@@ -30,6 +30,8 @@ const RETRY_INTERVAL: Duration = Duration::from_secs(5);
 const BLOCK_BATCH_SIZE: u64 = 100;
 /// How much to wait before polling the next latest block
 const POLL_INTERVAL: Duration = Duration::from_secs(2);
+/// Retry last N blocks
+const RETRY_PREVIOUS_BLOCKS: u64 = 100;
 
 #[derive(Debug, Clone)]
 pub struct BlockRange {
@@ -194,7 +196,6 @@ pub async fn watch_blocks_wrapper(
     chain_state: BlockchainState,
     latest_safe_block: BlockNumber,
     tx: mpsc::Sender<BlockRange>,
-    retry_previous_blocks: u64,
 ) {
     let mut last_safe_block_processed = latest_safe_block;
     loop {
@@ -202,7 +203,6 @@ pub async fn watch_blocks_wrapper(
             chain_state.clone(),
             &mut last_safe_block_processed,
             tx.clone(),
-            retry_previous_blocks,
         )
         .in_current_span()
         .await
@@ -221,7 +221,6 @@ pub async fn watch_blocks(
     chain_state: BlockchainState,
     last_safe_block_processed: &mut BlockNumber,
     tx: mpsc::Sender<BlockRange>,
-    retry_previous_blocks: u64,
 ) -> Result<()> {
     tracing::info!("Watching blocks to handle new events");
 
@@ -230,7 +229,7 @@ pub async fn watch_blocks(
 
         let latest_safe_block = get_latest_safe_block(&chain_state).in_current_span().await;
         if latest_safe_block > *last_safe_block_processed {
-            let mut from = latest_safe_block.saturating_sub(retry_previous_blocks);
+            let mut from = latest_safe_block.saturating_sub(RETRY_PREVIOUS_BLOCKS);
 
             // In normal situation, the difference between latest and last safe block should not be more than 2-3 (for arbitrum it can be 10)
             // TODO: add a metric for this in separate PR. We need alerts

+ 13 - 0
apps/fortuna/src/keeper/process_event.rs

@@ -42,6 +42,7 @@ pub async fn process_event_with_backoff(
     tracing::info!("Started processing event");
     let mut status = RequestStatus {
         chain_id: chain_state.id.clone(),
+        network_id: chain_state.network_id,
         provider: event.provider_address,
         sequence: event.sequence_number,
         created_at: chrono::Utc::now(),
@@ -51,6 +52,7 @@ pub async fn process_event_with_backoff(
         sender: event.requestor,
         user_random_number: event.user_random_number,
         state: RequestEntryState::Pending,
+        gas_limit,
     };
     history.add(&status);
 
@@ -60,6 +62,7 @@ pub async fn process_event_with_backoff(
         .map_err(|e| {
             status.state = RequestEntryState::Failed {
                 reason: format!("Error revealing: {:?}", e),
+                provider_random_number: None,
             };
             history.add(&status);
             anyhow!("Error revealing: {:?}", e)
@@ -91,6 +94,11 @@ pub async fn process_event_with_backoff(
                 reveal_block_number: result.receipt.block_number.unwrap_or_default().as_u64(),
                 reveal_tx_hash: result.receipt.transaction_hash,
                 provider_random_number: provider_revelation,
+                gas_used: result.receipt.gas_used.unwrap_or_default(),
+                combined_random_number: RequestStatus::generate_combined_random_number(
+                    &event.user_random_number,
+                    &provider_revelation,
+                ),
             };
             history.add(&status);
             tracing::info!(
@@ -160,6 +168,11 @@ pub async fn process_event_with_backoff(
                     .requests_processed_failure
                     .get_or_create(&account_label)
                     .inc();
+                status.state = RequestEntryState::Failed {
+                    reason: format!("Error revealing: {:?}", e),
+                    provider_random_number: Some(provider_revelation),
+                };
+                history.add(&status);
             }
         }
     }

+ 1 - 0
apps/fortuna/src/lib.rs

@@ -5,4 +5,5 @@ pub mod config;
 pub mod eth_utils;
 pub mod history;
 pub mod keeper;
+pub mod serde;
 pub mod state;

+ 21 - 0
apps/fortuna/src/serde.rs

@@ -0,0 +1,21 @@
+pub mod u256 {
+    use {
+        ethers::types::U256,
+        serde::{de::Error, Deserialize, Deserializer, Serializer},
+    };
+
+    pub fn serialize<S>(b: &U256, s: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        s.serialize_str(b.to_string().as_str())
+    }
+
+    pub fn deserialize<'de, D>(d: D) -> Result<U256, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let s: String = Deserialize::deserialize(d)?;
+        U256::from_dec_str(s.as_str()).map_err(|err| D::Error::custom(err.to_string()))
+    }
+}