Переглянути джерело

Merge branch 'main' into stylus-target-chain-structure

Ayush Suresh 5 місяців тому
батько
коміт
3d3b87a85f
63 змінених файлів з 5143 додано та 1104 видалено
  1. 2 0
      apps/fortuna/.gitignore
  2. 0 116
      apps/fortuna/.sqlx/query-392da9e5fdd212a4a665c86e5fc6d4f619355294490248e656ad0fc97a252471.json
  3. 0 116
      apps/fortuna/.sqlx/query-78be8c62d5eb764995221f927b0f166e38d6fba8eb8fddb07f50c572fd27b4e2.json
  4. 0 116
      apps/fortuna/.sqlx/query-8cd10cd5839b81bd9538aeb10fdfd27c6e36baf5d90a4fb9e61718f021812710.json
  5. 0 116
      apps/fortuna/.sqlx/query-905dbc91cd5319537c5c194277d531689ac5c1338396414467496d0f50ddc3f0.json
  6. 0 116
      apps/fortuna/.sqlx/query-a62e094cee65ae58bd12ce7d3e7df44f5aca31520d1ceced83f492945e850764.json
  7. 0 116
      apps/fortuna/.sqlx/query-c9e3089b1ffd52d20cfcd89e71e051c0f351643dce9be4b84b6343909c816c22.json
  8. 0 116
      apps/fortuna/.sqlx/query-f58bdd3e0ecb30f35356c22e9ab1b3802f8eebda455efabc18d30f02d23787b7.json
  9. 2 2
      apps/fortuna/Cargo.lock
  10. 1 1
      apps/fortuna/Cargo.toml
  11. 9 0
      apps/fortuna/README.md
  12. 149 0
      apps/fortuna/flake.lock
  13. 63 0
      apps/fortuna/flake.nix
  14. 22 0
      apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.down.sql
  15. 22 0
      apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.up.sql
  16. 4 0
      apps/fortuna/migrations/20250605165549_re-add_tx_hash_indices.down.sql
  17. 4 0
      apps/fortuna/migrations/20250605165549_re-add_tx_hash_indices.up.sql
  18. 12 0
      apps/fortuna/src/api.rs
  19. 138 53
      apps/fortuna/src/api/explorer.rs
  20. 2 0
      apps/fortuna/src/command/run.rs
  21. 9 2
      apps/fortuna/src/config.rs
  22. 2 7
      apps/fortuna/src/eth_utils/traced_client.rs
  23. 424 163
      apps/fortuna/src/history.rs
  24. 7 12
      apps/fortuna/src/keeper/keeper_metrics.rs
  25. 22 20
      apps/insights/src/components/LivePrices/index.tsx
  26. 6 1
      apps/insights/src/components/PriceFeed/chart.tsx
  27. 40 0
      apps/insights/src/hooks/use-price-formatter.ts
  28. 1 1
      apps/price_pusher/package.json
  29. 1 0
      apps/price_pusher/src/aptos/aptos.ts
  30. 1 0
      apps/price_pusher/src/fuel/fuel.ts
  31. 1 0
      apps/price_pusher/src/injective/injective.ts
  32. 1 0
      apps/price_pusher/src/near/near.ts
  33. 6 3
      apps/price_pusher/src/price-config.ts
  34. 22 11
      apps/price_pusher/src/pyth-price-listener.ts
  35. 1 0
      apps/price_pusher/src/solana/solana.ts
  36. 1 0
      apps/price_pusher/src/sui/sui.ts
  37. 1 0
      apps/price_pusher/src/ton/ton.ts
  38. 0 12
      contract_manager/store/tokens/Tokens.json
  39. 0 1
      flake.nix
  40. 10 1
      governance/xc_admin/packages/xc_admin_cli/src/index.ts
  41. 1 1
      lazer/Cargo.lock
  42. 1 0
      lazer/contracts/evm/foundry.toml
  43. 136 0
      lazer/publisher_sdk/proto/state.proto
  44. 1 1
      lazer/publisher_sdk/rust/Cargo.toml
  45. 4 0
      lazer/publisher_sdk/rust/src/lib.rs
  46. 2 0
      packages/known-publishers/src/icons/dark/amber.svg
  47. 2 0
      packages/known-publishers/src/icons/light/amber.svg
  48. 1 0
      packages/known-publishers/src/icons/monochrome/amber.svg
  49. 11 0
      packages/known-publishers/src/index.tsx
  50. 2 0
      pyth-lazer-agent/.dockerignore
  51. 6 0
      pyth-lazer-agent/.gitignore
  52. 3204 0
      pyth-lazer-agent/Cargo.lock
  53. 34 0
      pyth-lazer-agent/Cargo.toml
  54. 17 0
      pyth-lazer-agent/Dockerfile
  55. 5 0
      pyth-lazer-agent/config/config.toml
  56. 4 0
      pyth-lazer-agent/rust-toolchain.toml
  57. 37 0
      pyth-lazer-agent/src/config.rs
  58. 120 0
      pyth-lazer-agent/src/http_server.rs
  59. 150 0
      pyth-lazer-agent/src/lazer_publisher.rs
  60. 46 0
      pyth-lazer-agent/src/main.rs
  61. 173 0
      pyth-lazer-agent/src/publisher_handle.rs
  62. 157 0
      pyth-lazer-agent/src/relayer_session.rs
  63. 43 0
      pyth-lazer-agent/src/websocket_utils.rs

+ 2 - 0
apps/fortuna/.gitignore

@@ -2,3 +2,5 @@
 *config.yaml
 *secret*
 *private-key*
+.envrc
+fortuna.db

+ 0 - 116
apps/fortuna/.sqlx/query-392da9e5fdd212a4a665c86e5fc6d4f619355294490248e656ad0fc97a252471.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-78be8c62d5eb764995221f927b0f166e38d6fba8eb8fddb07f50c572fd27b4e2.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-8cd10cd5839b81bd9538aeb10fdfd27c6e36baf5d90a4fb9e61718f021812710.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-905dbc91cd5319537c5c194277d531689ac5c1338396414467496d0f50ddc3f0.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-a62e094cee65ae58bd12ce7d3e7df44f5aca31520d1ceced83f492945e850764.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-c9e3089b1ffd52d20cfcd89e71e051c0f351643dce9be4b84b6343909c816c22.json

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

+ 0 - 116
apps/fortuna/.sqlx/query-f58bdd3e0ecb30f35356c22e9ab1b3802f8eebda455efabc18d30f02d23787b7.json

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

+ 2 - 2
apps/fortuna/Cargo.lock

@@ -3108,9 +3108,9 @@ dependencies = [
 
 [[package]]
 name = "prometheus-client"
-version = "0.21.2"
+version = "0.23.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c99afa9a01501019ac3a14d71d9f94050346f55ca471ce90c799a15c58f61e2"
+checksum = "cf41c1a7c32ed72abe5082fb19505b969095c12da9f5732a4bc9878757fd087c"
 dependencies = [
  "dtoa",
  "itoa",

+ 1 - 1
apps/fortuna/Cargo.toml

@@ -19,7 +19,7 @@ ethabi = "18.0.0"
 ethers = { version = "2.0.14", features = ["ws"] }
 futures = { version = "0.3.28" }
 hex = "0.4.3"
-prometheus-client = { version = "0.21.2" }
+prometheus-client = { version = "0.23.1" }
 pythnet-sdk = { path = "../../pythnet/pythnet_sdk", features = ["strum"] }
 rand = "0.8.5"
 reqwest = { version = "0.11.22", features = ["json", "blocking"] }

+ 9 - 0
apps/fortuna/README.md

@@ -57,3 +57,12 @@ RUST_LOG=INFO cargo run -- run
 ```
 
 This command will start the webservice on `localhost:34000`.
+
+## Nix
+
+If you are a nix user, you can use the included [Nix flake](./flake.nix) and
+[direnv config](./envrc) which will configure your environment for you
+automatically.  If you use this configuration you will have a `cli` script in
+your dev shell which provides easy access to some common tasks, such as `cli
+start` to start the server in watch mode, `cli test` to run unit, code format,
+and lint checks, and `cli fix` to run auto-fixes for formatting and lint issues.

+ 149 - 0
apps/fortuna/flake.lock

@@ -0,0 +1,149 @@
+{
+  "nodes": {
+    "flake-utils": {
+      "inputs": {
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "flake-utils_2": {
+      "inputs": {
+        "systems": "systems_2"
+      },
+      "locked": {
+        "lastModified": 1710146030,
+        "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "mkCli": {
+      "inputs": {
+        "flake-utils": "flake-utils_2",
+        "nixpkgs": "nixpkgs"
+      },
+      "locked": {
+        "lastModified": 1722285416,
+        "narHash": "sha256-Yb1NV8zVt9gMnlEni8IFLtLhGkqPDYAoZxi7ZxiZq0A=",
+        "owner": "cprussin",
+        "repo": "mkCli",
+        "rev": "983799699e84d2cb428b65b8f628663bfc82fbb2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cprussin",
+        "repo": "mkCli",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1718310279,
+        "narHash": "sha256-OfRCrNPMLlElYIUAWIOGAvJ5XDT/WpOXj6Ymfttj6Lk=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "5dde575c3c4afb618781387b0db6d1c08a206c92",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1748799545,
+        "narHash": "sha256-IcYNrKd4yztiJv2B3EKy70qf1vNBKxnXhf4j0ntMSAM=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "83ae70ea0415026d51d4a12bb82c962a2eaab5b0",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "flake-utils": "flake-utils",
+        "mkCli": "mkCli",
+        "nixpkgs": "nixpkgs_2",
+        "rust-overlay": "rust-overlay"
+      }
+    },
+    "rust-overlay": {
+      "inputs": {
+        "nixpkgs": [
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1748746145,
+        "narHash": "sha256-bwkCAK9pOyI2Ww4Q4oO1Ynv7O9aZPrsIAMMASmhVGp4=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "12a0d94a2f2b06714f747ab97b2fa546f46b460c",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "systems_2": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}

+ 63 - 0
apps/fortuna/flake.nix

@@ -0,0 +1,63 @@
+{
+  inputs = {
+    nixpkgs.url = "github:NixOS/nixpkgs";
+    flake-utils.url = "github:numtide/flake-utils";
+    mkCli.url = "github:cprussin/mkCli";
+    rust-overlay = {
+      url = "github:oxalica/rust-overlay";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+  };
+
+  outputs = {
+    nixpkgs,
+    flake-utils,
+    mkCli,
+    rust-overlay,
+    ...
+  }: (
+    flake-utils.lib.eachDefaultSystem
+    (
+      system: let
+        cli-overlay = nixpkgs.lib.composeExtensions mkCli.overlays.default (_: prev: let
+          cargo = "cargo --color=always";
+        in {
+          cli = prev.lib.mkCli "cli" {
+            _noAll = true;
+            start = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && RUST_LOG=info ${cargo} watch -x 'run -- run'";
+            test = {
+              format = "${cargo} fmt --check";
+              lint = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} clippy --color always";
+              unit = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} test -- --color always";
+            };
+            fix = {
+              format = "${cargo} fmt";
+              lint = "${cargo} sqlx migrate run && ${cargo} sqlx prepare && ${cargo} clippy -- --color always --fix";
+            };
+          };
+        });
+
+        pkgs = import nixpkgs {
+          inherit system;
+          overlays = [cli-overlay rust-overlay.overlays.default];
+          config = {};
+        };
+      in {
+        devShells.default = pkgs.mkShell {
+          buildInputs = [
+            pkgs.cli
+            pkgs.git
+            pkgs.openssl
+            pkgs.pkg-config
+            pkgs.rust-analyzer
+            pkgs.rust-bin.stable."1.82.0".default
+            pkgs.sqlx-cli
+            pkgs.foundry
+            pkgs.sqlite
+            pkgs.cargo-watch
+          ];
+        };
+      }
+    )
+  );
+}

+ 22 - 0
apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.down.sql

@@ -0,0 +1,22 @@
+-- Add down migration script here
+
+DROP INDEX request__network_id__state__created_at;
+DROP INDEX request__network_id__created_at;
+DROP INDEX request__sender__network_id__state__created_at;
+DROP INDEX request__sender__network_id__created_at;
+DROP INDEX request__sender__state__created_at;
+DROP INDEX request__sender__created_at;
+DROP INDEX request__sequence__network_id__state__created_at;
+DROP INDEX request__sequence__network_id__created_at;
+DROP INDEX request__sequence__state__created_at;
+DROP INDEX request__sequence__created_at;
+DROP INDEX request__state__created_at;
+DROP INDEX request__created_at;
+
+
+CREATE INDEX idx_request_sequence ON request (sequence);
+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;
+CREATE INDEX idx_request_sender ON request (sender) WHERE sender IS NOT NULL;

+ 22 - 0
apps/fortuna/migrations/20250605004757_add_indices_for_advanced_search.up.sql

@@ -0,0 +1,22 @@
+-- Add up migration script here
+
+DROP INDEX idx_request_sequence;
+DROP INDEX idx_request_network_id_created_at;
+DROP INDEX idx_request_created_at;
+DROP INDEX idx_request_request_tx_hash;
+DROP INDEX idx_request_reveal_tx_hash;
+DROP INDEX idx_request_sender;
+
+
+CREATE INDEX request__network_id__state__created_at ON request(network_id, state, created_at);
+CREATE INDEX request__network_id__created_at ON request(network_id, created_at);
+CREATE INDEX request__sender__network_id__state__created_at ON request(sender, network_id, state, created_at);
+CREATE INDEX request__sender__network_id__created_at ON request(sender, network_id, created_at);
+CREATE INDEX request__sender__state__created_at ON request(sender, state, created_at);
+CREATE INDEX request__sender__created_at ON request(sender, created_at);
+CREATE INDEX request__sequence__network_id__state__created_at ON request(sequence, network_id, state, created_at);
+CREATE INDEX request__sequence__network_id__created_at ON request(sequence, network_id, created_at);
+CREATE INDEX request__sequence__state__created_at ON request(sequence, state, created_at);
+CREATE INDEX request__sequence__created_at ON request(sequence, created_at);
+CREATE INDEX request__state__created_at ON request(state, created_at);
+CREATE INDEX request__created_at ON request(created_at);

+ 4 - 0
apps/fortuna/migrations/20250605165549_re-add_tx_hash_indices.down.sql

@@ -0,0 +1,4 @@
+-- Add down migration script here
+
+DROP INDEX request__request_tx_hash;
+DROP INDEX request__reveal_tx_hash;

+ 4 - 0
apps/fortuna/migrations/20250605165549_re-add_tx_hash_indices.up.sql

@@ -0,0 +1,4 @@
+-- Add up migration script here
+
+CREATE INDEX request__request_tx_hash ON request (request_tx_hash) WHERE request_tx_hash IS NOT NULL;
+CREATE INDEX request__reveal_tx_hash ON request (reveal_tx_hash) WHERE reveal_tx_hash IS NOT NULL;

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

@@ -35,6 +35,13 @@ mod revelation;
 pub type ChainId = String;
 pub type NetworkId = u64;
 
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema, sqlx::Type)]
+pub enum StateTag {
+    Pending,
+    Completed,
+    Failed,
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
 pub struct RequestLabel {
     pub value: String,
@@ -54,6 +61,8 @@ pub struct ApiState {
 
     /// Prometheus metrics
     pub metrics: Arc<ApiMetrics>,
+
+    pub explorer_metrics: Arc<ExplorerMetrics>,
 }
 
 impl ApiState {
@@ -66,6 +75,8 @@ impl ApiState {
             http_requests: Family::default(),
         };
 
+        let explorer_metrics = Arc::new(ExplorerMetrics::new(metrics_registry.clone()).await);
+
         let http_requests = metrics.http_requests.clone();
         metrics_registry.write().await.register(
             "http_requests",
@@ -76,6 +87,7 @@ impl ApiState {
         ApiState {
             chains,
             metrics: Arc::new(metrics),
+            explorer_metrics,
             history,
             metrics_registry,
         }

+ 138 - 53
apps/fortuna/src/api/explorer.rs

@@ -1,19 +1,89 @@
 use {
     crate::{
-        api::{ApiBlockChainState, NetworkId, RestError},
-        history::RequestStatus,
+        api::{ApiBlockChainState, NetworkId, RestError, StateTag},
+        config::LATENCY_BUCKETS,
+        history::{RequestQueryBuilder, RequestStatus, SearchField},
     },
     axum::{
         extract::{Query, State},
         Json,
     },
     chrono::{DateTime, Utc},
-    ethers::types::{Address, TxHash},
-    std::str::FromStr,
+    prometheus_client::{
+        encoding::{EncodeLabelSet, EncodeLabelValue},
+        metrics::{family::Family, histogram::Histogram},
+        registry::Registry,
+    },
+    std::sync::Arc,
+    tokio::{sync::RwLock, time::Instant},
     utoipa::IntoParams,
 };
 
-#[derive(Debug, serde::Serialize, serde::Deserialize, IntoParams)]
+#[derive(Debug)]
+pub struct ExplorerMetrics {
+    results_latency: Family<QueryTags, Histogram>,
+    count_latency: Family<QueryTags, Histogram>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)]
+pub struct QueryTags {
+    search_type: Option<SearchType>,
+    has_network_id_filter: bool,
+    has_state_filter: bool,
+}
+
+impl<'a> From<RequestQueryBuilder<'a>> for QueryTags {
+    fn from(builder: RequestQueryBuilder<'a>) -> Self {
+        QueryTags {
+            search_type: builder.search.map(|val| match val {
+                SearchField::TxHash(_) => SearchType::TxHash,
+                SearchField::Sender(_) => SearchType::Sender,
+                SearchField::SequenceNumber(_) => SearchType::SequenceNumber,
+            }),
+            has_network_id_filter: builder.network_id.is_some(),
+            has_state_filter: builder.state.is_some(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelValue)]
+enum SearchType {
+    TxHash,
+    Sender,
+    SequenceNumber,
+}
+
+impl ExplorerMetrics {
+    pub async fn new(metrics_registry: Arc<RwLock<Registry>>) -> Self {
+        let mut guard = metrics_registry.write().await;
+        let sub_registry = guard.sub_registry_with_prefix("explorer");
+
+        let results_latency = Family::<QueryTags, Histogram>::new_with_constructor(|| {
+            Histogram::new(LATENCY_BUCKETS.into_iter())
+        });
+        sub_registry.register(
+            "results_latency",
+            "The latency of requests to the database to collect the limited results.",
+            results_latency.clone(),
+        );
+
+        let count_latency = Family::<QueryTags, Histogram>::new_with_constructor(|| {
+            Histogram::new(LATENCY_BUCKETS.into_iter())
+        });
+        sub_registry.register(
+            "count_latency",
+            "The latency of requests to the database to collect the total matching result count.",
+            count_latency.clone(),
+        );
+
+        Self {
+            results_latency,
+            count_latency,
+        }
+    }
+}
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, IntoParams)]
 #[into_params(parameter_in=Query)]
 pub struct ExplorerQueryParams {
     /// Only return logs that are newer or equal to this timestamp. Timestamp is in ISO 8601 format with UTC timezone.
@@ -27,6 +97,8 @@ pub struct ExplorerQueryParams {
     /// The network ID to filter the results by.
     #[param(value_type = Option<u64>)]
     pub network_id: Option<NetworkId>,
+    /// The state to filter the results by.
+    pub state: Option<StateTag>,
     /// The maximum number of logs to return. Max value is 1000.
     #[param(default = 1000)]
     pub limit: Option<u64>,
@@ -35,7 +107,11 @@ pub struct ExplorerQueryParams {
     pub offset: Option<u64>,
 }
 
-const LOG_RETURN_LIMIT: u64 = 1000;
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+pub struct ExplorerResponse {
+    pub requests: Vec<RequestStatus>,
+    pub total_results: u64,
+}
 
 /// Returns the logs of all requests captured by the keeper.
 ///
@@ -44,13 +120,13 @@ const LOG_RETURN_LIMIT: u64 = 1000;
 #[utoipa::path(
     get,
     path = "/v1/logs",
-    responses((status = 200, description = "A list of Entropy request logs", body = Vec<RequestStatus>)),
+    responses((status = 200, description = "A list of Entropy request logs", body = ExplorerResponse)),
     params(ExplorerQueryParams)
 )]
 pub async fn explorer(
     State(state): State<crate::api::ApiState>,
     Query(query_params): Query<ExplorerQueryParams>,
-) -> anyhow::Result<Json<Vec<RequestStatus>>, RestError> {
+) -> anyhow::Result<Json<ExplorerResponse>, RestError> {
     if let Some(network_id) = &query_params.network_id {
         if !state
             .chains
@@ -65,52 +141,61 @@ pub async fn explorer(
             return Err(RestError::InvalidChainId);
         }
     }
+    let mut query = state.history.query();
+    if let Some(search) = query_params.query {
+        query = query
+            .search(search)
+            .map_err(|_| RestError::InvalidQueryString)?;
+    }
+    if let Some(network_id) = query_params.network_id {
+        query = query.network_id(network_id);
+    }
+    if let Some(state) = query_params.state {
+        query = query.state(state);
+    }
     if let Some(limit) = query_params.limit {
-        if limit > LOG_RETURN_LIMIT || limit == 0 {
-            return Err(RestError::InvalidQueryString);
-        }
+        query = query
+            .limit(limit)
+            .map_err(|_| RestError::InvalidQueryString)?;
     }
-    if let Some(query) = query_params.query {
-        if let Ok(tx_hash) = TxHash::from_str(&query) {
-            return Ok(Json(
-                state
-                    .history
-                    .get_requests_by_tx_hash(tx_hash)
-                    .await
-                    .map_err(|_| RestError::TemporarilyUnavailable)?,
-            ));
-        }
-        if let Ok(sender) = Address::from_str(&query) {
-            return Ok(Json(
-                state
-                    .history
-                    .get_requests_by_sender(sender, query_params.network_id)
-                    .await
-                    .map_err(|_| RestError::TemporarilyUnavailable)?,
-            ));
-        }
-        if let Ok(sequence_number) = u64::from_str(&query) {
-            return Ok(Json(
-                state
-                    .history
-                    .get_requests_by_sequence(sequence_number, query_params.network_id)
-                    .await
-                    .map_err(|_| RestError::TemporarilyUnavailable)?,
-            ));
-        }
-        return Err(RestError::InvalidQueryString);
+    if let Some(offset) = query_params.offset {
+        query = query.offset(offset);
     }
-    Ok(Json(
-        state
-            .history
-            .get_requests_by_time(
-                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,
-            )
-            .await
-            .map_err(|_| RestError::TemporarilyUnavailable)?,
-    ))
+    if let Some(min_timestamp) = query_params.min_timestamp {
+        query = query.min_timestamp(min_timestamp);
+    }
+    if let Some(max_timestamp) = query_params.max_timestamp {
+        query = query.max_timestamp(max_timestamp);
+    }
+
+    let results_latency = &state.explorer_metrics.results_latency;
+    let count_latency = &state.explorer_metrics.count_latency;
+    let query_tags = &query.clone().into();
+    let (requests, total_results) = tokio::join!(
+        measure_latency(results_latency, query_tags, query.execute()),
+        measure_latency(count_latency, query_tags, query.count_results())
+    );
+    let requests = requests.map_err(|_| RestError::TemporarilyUnavailable)?;
+    let total_results = total_results.map_err(|_| RestError::TemporarilyUnavailable)?;
+
+    Ok(Json(ExplorerResponse {
+        requests,
+        total_results,
+    }))
+}
+
+async fn measure_latency<T, F>(
+    metric: &Family<QueryTags, Histogram>,
+    query_tags: &QueryTags,
+    function: F,
+) -> T
+where
+    F: std::future::Future<Output = T>,
+{
+    let start = Instant::now();
+    let return_value = function.await;
+    metric
+        .get_or_create(query_tags)
+        .observe(start.elapsed().as_secs_f64());
+    return_value
 }

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

@@ -44,6 +44,8 @@ pub async fn run_api(
     crate::history::RequestEntryState,
     crate::api::Blob,
     crate::api::BinaryEncoding,
+    crate::api::StateTag,
+    crate::api::ExplorerResponse,
     )
     ),
     tags(

+ 9 - 2
apps/fortuna/src/config.rs

@@ -11,8 +11,9 @@ use {
 };
 pub use {
     generate::GenerateOptions, get_request::GetRequestOptions, inspect::InspectOptions,
-    register_provider::RegisterProviderOptions, request_randomness::RequestRandomnessOptions,
-    run::RunOptions, setup_provider::SetupProviderOptions, withdraw_fees::WithdrawFeesOptions,
+    prometheus_client::metrics::histogram::Histogram, register_provider::RegisterProviderOptions,
+    request_randomness::RequestRandomnessOptions, run::RunOptions,
+    setup_provider::SetupProviderOptions, withdraw_fees::WithdrawFeesOptions,
 };
 
 mod generate;
@@ -367,3 +368,9 @@ impl SecretString {
         Ok(None)
     }
 }
+
+/// This is a histogram with a bucket configuration appropriate for most things
+/// which measure latency to external services.
+pub const LATENCY_BUCKETS: [f64; 11] = [
+    0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0,
+];

+ 2 - 7
apps/fortuna/src/eth_utils/traced_client.rs

@@ -1,5 +1,5 @@
 use {
-    crate::api::ChainId,
+    crate::{api::ChainId, config::LATENCY_BUCKETS},
     anyhow::Result,
     axum::async_trait,
     ethers::{
@@ -42,12 +42,7 @@ impl RpcMetrics {
         );
 
         let latency = Family::<RpcLabel, Histogram>::new_with_constructor(|| {
-            Histogram::new(
-                [
-                    0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0,
-                ]
-                .into_iter(),
-            )
+            Histogram::new(LATENCY_BUCKETS.into_iter())
         });
         sub_registry.register(
             "latency",

+ 424 - 163
apps/fortuna/src/history.rs

@@ -1,5 +1,5 @@
 use {
-    crate::api::{ChainId, NetworkId},
+    crate::api::{ChainId, NetworkId, StateTag},
     anyhow::Result,
     chrono::{DateTime, NaiveDateTime},
     ethers::{
@@ -10,12 +10,14 @@ use {
     },
     serde::Serialize,
     serde_with::serde_as,
-    sqlx::{migrate, Pool, Sqlite, SqlitePool},
-    std::sync::Arc,
+    sqlx::{migrate, FromRow, Pool, QueryBuilder, Sqlite, SqlitePool},
+    std::{str::FromStr, sync::Arc},
     tokio::{spawn, sync::mpsc},
     utoipa::ToSchema,
 };
 
+const LOG_RETURN_LIMIT: u64 = 1000;
+
 #[serde_as]
 #[derive(Clone, Debug, Serialize, ToSchema, PartialEq)]
 #[serde(tag = "state", rename_all = "kebab-case")]
@@ -97,7 +99,7 @@ impl RequestStatus {
     }
 }
 
-#[derive(Clone, Debug, Serialize, ToSchema, PartialEq)]
+#[derive(Clone, Debug, Serialize, ToSchema, PartialEq, FromRow)]
 struct RequestRow {
     chain_id: String,
     network_id: i64,
@@ -335,136 +337,178 @@ impl History {
         }
     }
 
-    pub async fn get_requests_by_tx_hash(&self, tx_hash: TxHash) -> Result<Vec<RequestStatus>> {
-        let tx_hash: String = tx_hash.encode_hex();
-        let rows = sqlx::query_as!(
-            RequestRow,
-            "SELECT * FROM request WHERE request_tx_hash = ? OR reveal_tx_hash = ?",
-            tx_hash,
-            tx_hash
-        )
-        .fetch_all(&self.pool)
-        .await
-        .map_err(|e| {
-            tracing::error!("Failed to fetch request by tx hash: {}", e);
-            e
-        })?;
-        Ok(rows.into_iter().filter_map(|row| row.into()).collect())
+    pub fn query(&self) -> RequestQueryBuilder {
+        RequestQueryBuilder::new(&self.pool)
     }
+}
 
-    pub async fn get_requests_by_sender(
-        &self,
-        sender: Address,
-        network_id: Option<NetworkId>,
-    ) -> Result<Vec<RequestStatus>> {
-        let sender: String = sender.encode_hex();
-        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 network_id = ?",
-                    sender,
-                    network_id,
-                )
-                .fetch_all(&self.pool)
-                .await
-            }
-            None => {
-                sqlx::query_as!(RequestRow, "SELECT * FROM request WHERE sender = ?", sender,)
-                    .fetch_all(&self.pool)
-                    .await
-            }
+#[derive(Debug, Clone)]
+pub struct RequestQueryBuilder<'a> {
+    pool: &'a Pool<Sqlite>,
+    pub search: Option<SearchField>,
+    pub network_id: Option<i64>,
+    pub state: Option<StateTag>,
+    pub limit: i64,
+    pub offset: i64,
+    pub min_timestamp: DateTime<chrono::Utc>,
+    pub max_timestamp: DateTime<chrono::Utc>,
+}
+
+impl<'a> RequestQueryBuilder<'a> {
+    fn new(pool: &'a Pool<Sqlite>) -> Self {
+        Self {
+            pool,
+            search: None,
+            network_id: None,
+            state: None,
+            limit: LOG_RETURN_LIMIT as i64,
+            offset: 0,
+            // UTC_MIN and UTC_MAX are not valid timestamps in SQLite
+            // So we need small and large enough timestamps to replace them
+            min_timestamp: "2012-12-12T12:12:12Z"
+                .parse::<DateTime<chrono::Utc>>()
+                .unwrap(),
+            max_timestamp: "2050-12-12T12:12:12Z"
+                .parse::<DateTime<chrono::Utc>>()
+                .unwrap(),
         }
-        .map_err(|e| {
-            tracing::error!("Failed to fetch request by sender: {}", e);
-            e
-        })?;
-        Ok(rows.into_iter().filter_map(|row| row.into()).collect())
     }
 
-    pub async fn get_requests_by_sequence(
-        &self,
-        sequence: u64,
-        network_id: Option<NetworkId>,
-    ) -> Result<Vec<RequestStatus>> {
-        let sequence = sequence 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 sequence = ? AND network_id = ?",
-                    sequence,
-                    network_id,
-                )
-                .fetch_all(&self.pool)
-                .await
-            }
-            None => {
-                sqlx::query_as!(
-                    RequestRow,
-                    "SELECT * FROM request WHERE sequence = ?",
-                    sequence,
-                )
-                .fetch_all(&self.pool)
-                .await
-            }
+    pub fn search(mut self, search: String) -> Result<Self, RequestQueryBuilderError> {
+        if let Ok(tx_hash) = TxHash::from_str(&search) {
+            Ok(SearchField::TxHash(tx_hash))
+        } else if let Ok(sender) = Address::from_str(&search) {
+            Ok(SearchField::Sender(sender))
+        } else if let Ok(sequence_number) = u64::from_str(&search) {
+            Ok(SearchField::SequenceNumber(sequence_number as i64))
+        } else {
+            Err(RequestQueryBuilderError::InvalidSearch)
         }
-        .map_err(|e| {
-            tracing::error!("Failed to fetch request by sequence: {}", e);
-            e
-        })?;
-        Ok(rows.into_iter().filter_map(|row| row.into()).collect())
+        .map(|search_field| {
+            self.search = Some(search_field);
+            self
+        })
     }
 
-    pub async fn get_requests_by_time(
-        &self,
-        network_id: Option<NetworkId>,
-        limit: u64,
-        offset: u64,
-        min_timestamp: Option<DateTime<chrono::Utc>>,
-        max_timestamp: Option<DateTime<chrono::Utc>>,
-    ) -> Result<Vec<RequestStatus>> {
-        // UTC_MIN and UTC_MAX are not valid timestamps in SQLite
-        // So we need small and large enough timestamps to replace them
-        let min_timestamp = min_timestamp.unwrap_or(
-            "2012-12-12T12:12:12Z"
-                .parse::<DateTime<chrono::Utc>>()
-                .unwrap(),
-        );
-        let max_timestamp = max_timestamp.unwrap_or(
-            "2050-12-12T12:12:12Z"
-                .parse::<DateTime<chrono::Utc>>()
-                .unwrap(),
-        );
-        let limit = limit as i64;
-        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,
-                offset).fetch_all(&self.pool).await
+    pub fn network_id(mut self, network_id: NetworkId) -> Self {
+        self.network_id = Some(network_id as i64);
+        self
+    }
+
+    pub fn state(mut self, state: StateTag) -> Self {
+        self.state = Some(state);
+        self
+    }
+
+    pub fn limit(mut self, limit: u64) -> Result<Self, RequestQueryBuilderError> {
+        if limit > LOG_RETURN_LIMIT {
+            Err(RequestQueryBuilderError::LimitTooLarge)
+        } else if limit == 0 {
+            Err(RequestQueryBuilderError::ZeroLimit)
+        } else {
+            self.limit = limit as i64;
+            Ok(self)
+        }
+    }
+
+    pub fn offset(mut self, offset: u64) -> Self {
+        self.offset = offset as i64;
+        self
+    }
+
+    pub fn min_timestamp(mut self, min_timestamp: DateTime<chrono::Utc>) -> Self {
+        self.min_timestamp = min_timestamp;
+        self
+    }
+
+    pub fn max_timestamp(mut self, max_timestamp: DateTime<chrono::Utc>) -> Self {
+        self.max_timestamp = max_timestamp;
+        self
+    }
+
+    pub async fn execute(&self) -> Result<Vec<RequestStatus>> {
+        let mut query_builder = self.build_query("*");
+        query_builder.push(" LIMIT ");
+        query_builder.push_bind(self.limit);
+        query_builder.push(" OFFSET ");
+        query_builder.push_bind(self.offset);
+
+        let result: sqlx::Result<Vec<RequestRow>> =
+            query_builder.build_query_as().fetch_all(self.pool).await;
+
+        if let Err(e) = &result {
+            tracing::error!("Failed to fetch request: {}", e);
+        }
+
+        Ok(result?.into_iter().filter_map(|row| row.into()).collect())
+    }
+
+    pub async fn count_results(&self) -> Result<u64> {
+        self.build_query("COUNT(*) AS count")
+            .build_query_scalar::<u64>()
+            .fetch_one(self.pool)
+            .await
+            .map_err(|err| err.into())
+    }
+
+    fn build_query(&self, columns: &str) -> QueryBuilder<Sqlite> {
+        let mut query_builder = QueryBuilder::new(format!(
+            "SELECT {columns} FROM request WHERE created_at BETWEEN "
+        ));
+        query_builder.push_bind(self.min_timestamp);
+        query_builder.push(" AND ");
+        query_builder.push_bind(self.max_timestamp);
+
+        match &self.search {
+            Some(SearchField::TxHash(tx_hash)) => {
+                let tx_hash: String = tx_hash.encode_hex();
+                query_builder.push(" AND (request_tx_hash = ");
+                query_builder.push_bind(tx_hash.clone());
+                query_builder.push(" OR reveal_tx_hash = ");
+                query_builder.push_bind(tx_hash);
+                query_builder.push(")");
+            }
+            Some(SearchField::Sender(sender)) => {
+                let sender: String = sender.encode_hex();
+                query_builder.push(" AND sender = ");
+                query_builder.push_bind(sender);
             }
-            None => {
-                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,
-                offset).fetch_all(&self.pool).await
+            Some(SearchField::SequenceNumber(sequence_number)) => {
+                query_builder.push(" AND sequence = ");
+                query_builder.push_bind(sequence_number);
             }
-        }.map_err(|e| {
-            tracing::error!("Failed to fetch request by time: {}", e);
-            e
-        })?;
-        Ok(rows.into_iter().filter_map(|row| row.into()).collect())
+            None => (),
+        }
+
+        if let Some(network_id) = &self.network_id {
+            query_builder.push(" AND network_id = ");
+            query_builder.push_bind(network_id);
+        }
+
+        if let Some(state) = &self.state {
+            query_builder.push(" AND state = ");
+            query_builder.push_bind(state);
+        }
+
+        query_builder.push(" ORDER BY created_at DESC");
+        query_builder
     }
 }
 
+#[derive(Debug)]
+pub enum RequestQueryBuilderError {
+    LimitTooLarge,
+    ZeroLimit,
+    InvalidSearch,
+}
+
+#[derive(Debug, Clone)]
+pub enum SearchField {
+    TxHash(TxHash),
+    Sender(Address),
+    SequenceNumber(i64),
+}
+
 #[cfg(test)]
 mod test {
     use {super::*, chrono::Duration, tokio::time::sleep};
@@ -505,37 +549,193 @@ mod test {
         History::update_request_status(&history.pool, status.clone()).await;
 
         let logs = history
-            .get_requests_by_sequence(status.sequence, Some(status.network_id))
+            .query()
+            .search(status.sequence.to_string())
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.sequence.to_string())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.sequence.to_string())
+            .unwrap()
+            .state(StateTag::Completed)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.request_tx_hash.encode_hex())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(format!(
+                "0x{}",
+                status.request_tx_hash.encode_hex::<String>()
+            ))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.request_tx_hash.encode_hex::<String>().to_uppercase())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(format!(
+                "0x{}",
+                status.request_tx_hash.encode_hex::<String>().to_uppercase()
+            ))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.request_tx_hash.encode_hex())
+            .unwrap()
+            .state(StateTag::Completed)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(reveal_tx_hash.encode_hex())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(format!("0x{}", reveal_tx_hash.encode_hex::<String>()))
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(reveal_tx_hash.encode_hex::<String>().to_uppercase())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_sequence(status.sequence, None)
+            .query()
+            .search(format!(
+                "0x{}",
+                reveal_tx_hash.encode_hex::<String>().to_uppercase()
+            ))
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_tx_hash(status.request_tx_hash)
+            .query()
+            .search(reveal_tx_hash.encode_hex())
+            .unwrap()
+            .state(StateTag::Completed)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_tx_hash(reveal_tx_hash)
+            .query()
+            .search(status.sender.encode_hex())
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_sender(status.sender, Some(status.network_id))
+            .query()
+            .search(format!("0x{}", status.sender.encode_hex::<String>()))
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
 
         let logs = history
-            .get_requests_by_sender(status.sender, None)
+            .query()
+            .search(status.sender.encode_hex::<String>().to_uppercase())
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(format!(
+                "0x{}",
+                status.sender.encode_hex::<String>().to_uppercase()
+            ))
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.sender.encode_hex())
+            .unwrap()
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![status.clone()]);
+
+        let logs = history
+            .query()
+            .search(status.sender.encode_hex())
+            .unwrap()
+            .state(StateTag::Completed)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
@@ -566,7 +766,10 @@ mod test {
         History::update_request_status(&history.pool, failed_status).await;
 
         let logs = history
-            .get_requests_by_tx_hash(reveal_tx_hash)
+            .query()
+            .search(reveal_tx_hash.encode_hex())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
@@ -583,7 +786,10 @@ mod test {
         };
         History::update_request_status(&history.pool, status.clone()).await;
         let logs = history
-            .get_requests_by_tx_hash(status.request_tx_hash)
+            .query()
+            .search(status.request_tx_hash.encode_hex())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status.clone()]);
@@ -617,31 +823,56 @@ mod test {
         History::update_request_status(&history.pool, status.clone()).await;
 
         let logs = history
-            .get_requests_by_sequence(status.sequence, Some(123))
+            .query()
+            .search(status.sequence.to_string())
+            .unwrap()
+            .network_id(123)
+            .execute()
+            .await
+            .unwrap();
+        assert_eq!(logs, vec![]);
+
+        let logs = history
+            .query()
+            .search((status.sequence + 1).to_string())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
 
         let logs = history
-            .get_requests_by_sequence(status.sequence + 1, None)
+            .query()
+            .search(TxHash::zero().encode_hex())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
 
         let logs = history
-            .get_requests_by_tx_hash(TxHash::zero())
+            .query()
+            .search(Address::zero().encode_hex())
+            .unwrap()
+            .network_id(status.network_id)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
 
         let logs = history
-            .get_requests_by_sender(Address::zero(), Some(status.network_id))
+            .query()
+            .search(Address::zero().encode_hex())
+            .unwrap()
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
 
         let logs = history
-            .get_requests_by_sender(Address::zero(), None)
+            .query()
+            .state(StateTag::Completed)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![]);
@@ -654,49 +885,41 @@ mod test {
         History::update_request_status(&history.pool, status.clone()).await;
         for network_id in [None, Some(121)] {
             // min = created_at = max
-            let logs = history
-                .get_requests_by_time(
-                    network_id,
-                    10,
-                    0,
-                    Some(status.created_at),
-                    Some(status.created_at),
-                )
+            let mut query = history.query().limit(10).unwrap();
+
+            if let Some(network_id) = network_id {
+                query = query.network_id(network_id);
+            }
+
+            let logs = query
+                .clone()
+                .min_timestamp(status.created_at)
+                .max_timestamp(status.created_at)
+                .execute()
                 .await
                 .unwrap();
             assert_eq!(logs, vec![status.clone()]);
 
             // min = created_at + 1
-            let logs = history
-                .get_requests_by_time(
-                    network_id,
-                    10,
-                    0,
-                    Some(status.created_at + Duration::seconds(1)),
-                    None,
-                )
+            let logs = query
+                .clone()
+                .min_timestamp(status.created_at + Duration::seconds(1))
+                .execute()
                 .await
                 .unwrap();
             assert_eq!(logs, vec![]);
 
             // max = created_at - 1
-            let logs = history
-                .get_requests_by_time(
-                    network_id,
-                    10,
-                    0,
-                    None,
-                    Some(status.created_at - Duration::seconds(1)),
-                )
+            let logs = query
+                .clone()
+                .max_timestamp(status.created_at - Duration::seconds(1))
+                .execute()
                 .await
                 .unwrap();
             assert_eq!(logs, vec![]);
 
             // no min or max
-            let logs = history
-                .get_requests_by_time(network_id, 10, 0, None, None)
-                .await
-                .unwrap();
+            let logs = query.execute().await.unwrap();
             assert_eq!(logs, vec![status.clone()]);
         }
     }
@@ -709,9 +932,47 @@ 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(121))
+            .query()
+            .search(1.to_string())
+            .unwrap()
+            .network_id(121)
+            .execute()
             .await
             .unwrap();
         assert_eq!(logs, vec![status]);
     }
+
+    #[tokio::test]
+    async fn test_count_results() {
+        let history = History::new_in_memory().await.unwrap();
+        History::update_request_status(&history.pool, get_random_request_status()).await;
+        History::update_request_status(&history.pool, get_random_request_status()).await;
+        let mut failed_status = get_random_request_status();
+        History::update_request_status(&history.pool, failed_status.clone()).await;
+        failed_status.state = RequestEntryState::Failed {
+            reason: "Failed".to_string(),
+            provider_random_number: None,
+        };
+        History::update_request_status(&history.pool, failed_status.clone()).await;
+
+        let results = history.query().count_results().await.unwrap();
+        assert_eq!(results, 3);
+
+        let results = history
+            .query()
+            .limit(1)
+            .unwrap()
+            .count_results()
+            .await
+            .unwrap();
+        assert_eq!(results, 3);
+
+        let results = history
+            .query()
+            .state(StateTag::Pending)
+            .count_results()
+            .await
+            .unwrap();
+        assert_eq!(results, 2);
+    }
 }

+ 7 - 12
apps/fortuna/src/keeper/keeper_metrics.rs

@@ -69,24 +69,19 @@ impl Default for KeeperMetrics {
             requests_reprocessed: Family::default(),
             reveals: Family::default(),
             request_duration_ms: Family::new_with_constructor(|| {
-                Histogram::new(
-                    vec![
-                        1000.0, 2500.0, 5000.0, 7500.0, 10000.0, 20000.0, 30000.0, 40000.0,
-                        50000.0, 60000.0, 120000.0, 180000.0, 240000.0, 300000.0, 600000.0,
-                    ]
-                    .into_iter(),
-                )
+                Histogram::new(vec![
+                    1000.0, 2500.0, 5000.0, 7500.0, 10000.0, 20000.0, 30000.0, 40000.0, 50000.0,
+                    60000.0, 120000.0, 180000.0, 240000.0, 300000.0, 600000.0,
+                ])
             }),
             retry_count: Family::new_with_constructor(|| {
-                Histogram::new(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 15.0, 20.0].into_iter())
+                Histogram::new(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 15.0, 20.0])
             }),
             final_gas_multiplier: Family::new_with_constructor(|| {
-                Histogram::new(
-                    vec![100.0, 125.0, 150.0, 200.0, 300.0, 400.0, 500.0, 600.0].into_iter(),
-                )
+                Histogram::new(vec![100.0, 125.0, 150.0, 200.0, 300.0, 400.0, 500.0, 600.0])
             }),
             final_fee_multiplier: Family::new_with_constructor(|| {
-                Histogram::new(vec![100.0, 110.0, 120.0, 140.0, 160.0, 180.0, 200.0].into_iter())
+                Histogram::new(vec![100.0, 110.0, 120.0, 140.0, 160.0, 180.0, 200.0])
             }),
             gas_price_estimate: Family::default(),
             highest_revealed_sequence_number: Family::default(),

+ 22 - 20
apps/insights/src/components/LivePrices/index.tsx

@@ -5,13 +5,14 @@ import type { PriceData, PriceComponent } from "@pythnetwork/client";
 import { Skeleton } from "@pythnetwork/component-library/Skeleton";
 import type { ReactNode } from "react";
 import { useMemo } from "react";
-import { useNumberFormatter, useDateFormatter } from "react-aria";
+import { useDateFormatter } from "react-aria";
 
 import styles from "./index.module.scss";
 import {
   useLivePriceComponent,
   useLivePriceData,
 } from "../../hooks/use-live-price-data";
+import { usePriceFormatter } from "../../hooks/use-price-formatter";
 import type { Cluster } from "../../services/pyth";
 
 export const SKELETON_WIDTH = 20;
@@ -66,20 +67,17 @@ const Price = ({
 }: {
   prev?: number | undefined;
   current?: number | undefined;
-}) => {
-  const numberFormatter = useNumberFormatter({ maximumFractionDigits: 5 });
-
-  return current === undefined ? (
+}) =>
+  current === undefined ? (
     <Skeleton width={SKELETON_WIDTH} />
   ) : (
     <span
       className={styles.price}
       data-direction={prev ? getChangeDirection(prev, current) : "flat"}
     >
-      {numberFormatter.format(current)}
+      <FormattedPriceValue n={current} />
     </span>
   );
-};
 
 export const LiveConfidence = ({
   publisherKey,
@@ -119,19 +117,23 @@ const LiveComponentConfidence = ({
   return <Confidence confidence={current?.latest.confidence} />;
 };
 
-const Confidence = ({ confidence }: { confidence?: number | undefined }) => {
-  const numberFormatter = useNumberFormatter({ maximumFractionDigits: 5 });
-
-  return (
-    <span className={styles.confidence}>
-      <PlusMinus className={styles.plusMinus} />
-      {confidence === undefined ? (
-        <Skeleton width={SKELETON_WIDTH} />
-      ) : (
-        <span>{numberFormatter.format(confidence)}</span>
-      )}
-    </span>
-  );
+const Confidence = ({ confidence }: { confidence?: number | undefined }) => (
+  <span className={styles.confidence}>
+    <PlusMinus className={styles.plusMinus} />
+    {confidence === undefined ? (
+      <Skeleton width={SKELETON_WIDTH} />
+    ) : (
+      <span>
+        <FormattedPriceValue n={confidence} />
+      </span>
+    )}
+  </span>
+);
+
+const FormattedPriceValue = ({ n }: { n: number }) => {
+  const formatter = usePriceFormatter();
+
+  return useMemo(() => formatter.format(n), [n, formatter]);
 };
 
 export const LiveLastUpdated = ({

+ 6 - 1
apps/insights/src/components/PriceFeed/chart.tsx

@@ -11,6 +11,7 @@ import { z } from "zod";
 
 import styles from "./chart.module.scss";
 import { useLivePriceData } from "../../hooks/use-live-price-data";
+import { usePriceFormatter } from "../../hooks/use-price-formatter";
 import { Cluster } from "../../services/pyth";
 
 type Props = {
@@ -44,6 +45,7 @@ const useChartElem = (symbol: string, feedId: string) => {
   const chartRef = useRef<ChartRefContents | undefined>(undefined);
   const earliestDateRef = useRef<bigint | undefined>(undefined);
   const isBackfilling = useRef(false);
+  const priceFormatter = usePriceFormatter();
 
   const backfillData = useCallback(() => {
     if (!isBackfilling.current && earliestDateRef.current) {
@@ -113,6 +115,9 @@ const useChartElem = (symbol: string, feedId: string) => {
           timeVisible: true,
           secondsVisible: true,
         },
+        localization: {
+          priceFormatter: priceFormatter.format,
+        },
       });
 
       const price = chart.addSeries(LineSeries, { priceFormat });
@@ -141,7 +146,7 @@ const useChartElem = (symbol: string, feedId: string) => {
         chart.remove();
       };
     }
-  }, [backfillData]);
+  }, [backfillData, priceFormatter]);
 
   useEffect(() => {
     if (current && chartRef.current) {

+ 40 - 0
apps/insights/src/hooks/use-price-formatter.ts

@@ -0,0 +1,40 @@
+import { useCallback, useMemo } from "react";
+import { useNumberFormatter } from "react-aria";
+
+export const usePriceFormatter = () => {
+  const bigNumberFormatter = useNumberFormatter({ maximumFractionDigits: 2 });
+  const smallNumberFormatter = useNumberFormatter({
+    maximumSignificantDigits: 5,
+  });
+  const format = useCallback(
+    (n: number) =>
+      n >= 1000
+        ? bigNumberFormatter.format(n)
+        : formatToSubscriptNumber(smallNumberFormatter.format(n)),
+    [bigNumberFormatter, smallNumberFormatter],
+  );
+  return useMemo(() => ({ format }), [format]);
+};
+
+const formatToSubscriptNumber = (numString: string) => {
+  const parts = numString.split(".");
+
+  const [integerPart, decimalPart] = parts;
+  if (integerPart && decimalPart) {
+    const zerosCount =
+      decimalPart.length - decimalPart.replace(/^0+/, "").length;
+
+    return zerosCount < 5
+      ? numString
+      : integerPart +
+          "." +
+          "0" +
+          (zerosCount > 9
+            ? String.fromCodePoint(0x20_80 + Math.floor(zerosCount / 10))
+            : "") +
+          String.fromCodePoint(0x20_80 + (zerosCount % 10)) +
+          decimalPart.replace(/^0+/, "");
+  } else {
+    return numString;
+  }
+};

+ 1 - 1
apps/price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "9.3.3",
+  "version": "9.3.4",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",

+ 1 - 0
apps/price_pusher/src/aptos/aptos.ts

@@ -109,6 +109,7 @@ export class AptosPricePusher implements IPricePusher {
   async getPriceFeedsUpdateData(priceIds: string[]): Promise<number[][]> {
     const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
       encoding: "base64",
+      ignoreInvalidPriceIds: true,
     });
     return response.binary.data.map((data) =>
       Array.from(Buffer.from(data, "base64")),

+ 1 - 0
apps/price_pusher/src/fuel/fuel.ts

@@ -101,6 +101,7 @@ export class FuelPricePusher implements IPricePusher {
     try {
       const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
         encoding: "base64",
+        ignoreInvalidPriceIds: true,
       });
       priceFeedUpdateData = response.binary.data;
     } catch (err: any) {

+ 1 - 0
apps/price_pusher/src/injective/injective.ts

@@ -301,6 +301,7 @@ export class InjectivePricePusher implements IPricePusher {
     try {
       const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
         encoding: "base64",
+        ignoreInvalidPriceIds: true,
       });
       const vaas = response.binary.data;
 

+ 1 - 0
apps/price_pusher/src/near/near.ts

@@ -131,6 +131,7 @@ export class NearPricePusher implements IPricePusher {
   ): Promise<string[]> {
     const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
       encoding: "base64",
+      ignoreInvalidPriceIds: true,
     });
     return response.binary.data;
   }

+ 6 - 3
apps/price_pusher/src/price-config.ts

@@ -96,9 +96,12 @@ export function shouldUpdate(
 ): UpdateCondition {
   const priceId = priceConfig.id;
 
-  // There is no price to update the target with.
+  // There is no price to update the target with. So we should not update it.
   if (sourceLatestPrice === undefined) {
-    return UpdateCondition.YES;
+    logger.info(
+      `${priceConfig.alias} (${priceId}) is not available on the source network. Ignoring it.`,
+    );
+    return UpdateCondition.NO;
   }
 
   // It means that price never existed there. So we should push the latest price feed.
@@ -140,7 +143,7 @@ export function shouldUpdate(
       }%? / early: < ${priceConfig.earlyUpdatePriceDeviation}%?) OR ` +
       `Confidence ratio: ${confidenceRatioPct.toFixed(5)}% (< ${
         priceConfig.confidenceRatio
-      }%? / early: < ${priceConfig.earlyUpdatePriceDeviation}%?)`,
+      }%? / early: < ${priceConfig.earlyUpdateConfidenceRatio}%?)`,
   );
 
   if (

+ 22 - 11
apps/price_pusher/src/pyth-price-listener.ts

@@ -5,6 +5,7 @@ import {
 } from "@pythnetwork/hermes-client";
 import { PriceInfo, IPriceListener, PriceItem } from "./interface";
 import { Logger } from "pino";
+import { sleep } from "./utils";
 
 type TimestampInMs = number & { readonly _: unique symbol };
 
@@ -34,6 +35,24 @@ export class PythPriceListener implements IPriceListener {
   // This method should be awaited on and once it finishes it has the latest value
   // for the given price feeds (if they exist).
   async start() {
+    this.startListening();
+
+    // Store health check interval reference
+    this.healthCheckInterval = setInterval(() => {
+      if (
+        this.lastUpdated === undefined ||
+        this.lastUpdated < Date.now() - 30 * 1000
+      ) {
+        throw new Error("Hermes Price feeds are not updating.");
+      }
+    }, 5000);
+  }
+
+  async startListening() {
+    this.logger.info(
+      `Starting to listen for price updates from Hermes for ${this.priceIds.length} price feeds.`,
+    );
+
     const eventSource = await this.hermesClient.getPriceUpdatesStream(
       this.priceIds,
       {
@@ -71,20 +90,12 @@ export class PythPriceListener implements IPriceListener {
       });
     };
 
-    eventSource.onerror = (error: Event) => {
+    eventSource.onerror = async (error: Event) => {
       console.error("Error receiving updates from Hermes:", error);
       eventSource.close();
+      await sleep(5000); // Wait a bit before trying to reconnect
+      this.startListening(); // Attempt to restart the listener
     };
-
-    // Store health check interval reference
-    this.healthCheckInterval = setInterval(() => {
-      if (
-        this.lastUpdated === undefined ||
-        this.lastUpdated < Date.now() - 30 * 1000
-      ) {
-        throw new Error("Hermes Price feeds are not updating.");
-      }
-    }, 5000);
   }
 
   getLatestPriceInfo(priceId: HexString): PriceInfo | undefined {

+ 1 - 0
apps/price_pusher/src/solana/solana.ts

@@ -118,6 +118,7 @@ export class SolanaPricePusher implements IPricePusher {
         shuffledPriceIds,
         {
           encoding: "base64",
+          ignoreInvalidPriceIds: true,
         },
       );
       priceFeedUpdateData = response.binary.data;

+ 1 - 0
apps/price_pusher/src/sui/sui.ts

@@ -225,6 +225,7 @@ export class SuiPricePusher implements IPricePusher {
           priceIdChunk,
           {
             encoding: "base64",
+            ignoreInvalidPriceIds: true,
           },
         );
         if (response.binary.data.length !== 1) {

+ 1 - 0
apps/price_pusher/src/ton/ton.ts

@@ -98,6 +98,7 @@ export class TonPricePusher implements IPricePusher {
     try {
       const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
         encoding: "base64",
+        ignoreInvalidPriceIds: true,
       });
       priceFeedUpdateData = response.binary.data;
     } catch (err: any) {

+ 0 - 12
contract_manager/store/tokens/Tokens.json

@@ -209,18 +209,6 @@
     "decimals": 18,
     "type": "token"
   },
-  {
-    "id": "WEMIX",
-    "pythId": "f63f008474fad630207a1cfa49207d59bca2593ea64fc0a6da9bf3337485791c",
-    "decimals": 18,
-    "type": "token"
-  },
-  {
-    "id": "EOS",
-    "pythId": "06ade621dbc31ed0fc9255caaab984a468abe84164fb2ccc76f02a4636d97e31",
-    "decimals": 18,
-    "type": "token"
-  },
   {
     "id": "RON",
     "pythId": "97cfe19da9153ef7d647b011c5e355142280ddb16004378573e6494e499879f3",

+ 0 - 1
flake.nix

@@ -48,7 +48,6 @@
       in {
         devShells.default = pkgs.mkShell {
           buildInputs = [
-            pkgs.cargo
             pkgs.cli
             pkgs.git
             pkgs.libusb1

+ 10 - 1
governance/xc_admin/packages/xc_admin_cli/src/index.ts

@@ -419,7 +419,16 @@ multisigCommand(
           ),
         ),
       )
-    ).flat();
+    )
+      .map((stakeAccounts, index) => {
+        if (stakeAccounts.length === 0) {
+          console.log(
+            `Skipping vote account ${voteAccounts[index].toBase58()} - no stake accounts found`,
+          );
+        }
+        return stakeAccounts;
+      })
+      .flat();
 
     const instructions = stakeAccounts.flatMap(
       (stakeAccount) =>

+ 1 - 1
lazer/Cargo.lock

@@ -3882,7 +3882,7 @@ dependencies = [
 
 [[package]]
 name = "pyth-lazer-publisher-sdk"
-version = "0.1.4"
+version = "0.1.5"
 dependencies = [
  "anyhow",
  "fs-err",

+ 1 - 0
lazer/contracts/evm/foundry.toml

@@ -5,3 +5,4 @@ libs = ["lib"]
 
 optimizer = true
 optimizer_runs = 100000
+solc_version = "0.8.23"

+ 136 - 0
lazer/publisher_sdk/proto/state.proto

@@ -0,0 +1,136 @@
+syntax = "proto3";
+package lazer;
+
+import "google/protobuf/duration.proto";
+import "google/protobuf/timestamp.proto";
+
+// All optional fields should always be set unless documented otherwise.
+
+// State of a Pyth Lazer shard.
+//
+// The state is shared across all Pyth Lazer aggregators that process this shard.
+// All aggregators should observe the same state at any `last_sequence_no`.
+// The state contains all the information necessary for processing the updates.
+// The aggregators cannot rely on any external data, except the state and the update sequence.
+// A snapshot of the state includes the serialized `State` value as the payload.
+message State {
+    // [required] ID of this shard. Each state value only accounts for data of a particular shard.
+    optional uint32 shard_id = 1;
+    // [required] sequence_no of the last update applied to the state.
+    optional uint64 last_sequence_no = 2;
+    // [required] Timestamp of the last update provided by Kafka/Nats.
+    // If no updates were applied, contains the timestamp of genesis snapshot creation time.
+    optional google.protobuf.Timestamp last_timestamp = 3;
+    // [required] Shard name (only for debug/monitoring/management purposes). Must be unique.
+    optional string shard_name = 4;
+    // [required] Minimal aggregation rate allowed in this shard.
+    optional google.protobuf.Duration min_rate = 5;
+    // List of feeds.
+    repeated Feed feeds = 7;
+    // List of publishers.
+    repeated Publisher publishers = 8;
+    // TODO: governance state (pubkey, last sequence no)
+}
+
+// An item of the state describing a publisher.
+message Publisher {
+    // [required] Publisher ID. Restricted to uint16.
+    optional uint32 publisher_id = 1;
+    // [required] Publisher name (only for debug/monitoring/management purposes). Must be unique.
+    optional string name = 2;
+    // Public keys used to sign publisher update transactions.
+    repeated bytes public_keys = 3;
+    // [required] If true, the publisher is active, i.e. it's allowed to publish updates.
+    optional bool is_active = 4;
+}
+
+// Static data for a feed.
+message FeedMetadata {
+    // [required] ID of the price feed.
+    optional uint32 price_feed_id = 1;
+    // [required] Feed name.
+    optional string name = 2;
+    // [required] Feed symbol.
+    optional string symbol = 3;
+    // [required] Feed description.
+    optional string description = 4;
+    // [required] Feed asset type.
+    optional string asset_type = 5;
+    // [required] Exponent applied to all price and rate values for this feed.
+    // Actual value is `mantissa * 10 ^ exponent`.
+    // Restricted to int16.
+    optional sint32 exponent = 6;
+    // [optional] CoinMarketCap ID. Can be absent if there is no CoinMarketCap ID for this symbol.
+    optional uint32 cmc_id = 7;
+    // [optional] Funding rate interval. Only present for funding rate feeds.
+    optional google.protobuf.Duration funding_rate_interval = 8;
+    // [required] Minimal number of publisher prices required to produce an aggregate.
+    optional uint32 min_publishers = 9;
+    // [required] Minimal rate of aggregation performed by the aggregator for this feed.
+    // Cannot be lower than the shard's top level `State.min_rate`.
+    optional google.protobuf.Duration min_rate = 10;
+    // [required] Time after which the publisher update is discarded.
+    optional google.protobuf.Duration expiry_time = 11;
+    // [required] If true, the feed is visible to the consumers. This can be used to prepare and verify
+    // new feeds before releasing them. This can also be used to migrate a feed from
+    // one shard to another. If a feed is present in
+    // multiple shards, it must only be active in one of them at each time.
+    // To enforce this, `pending_activation` and `pending_deactivation` fields
+    // can be used to deactivate a feed in one shard and activate it in another shard
+    // at the same instant.
+    optional bool is_activated = 12;
+    // [optional] ID of the corresponding price feed in Hermes (Pythnet).
+    optional string hermes_id = 13;
+    // [optional] Quote currency of the asset.
+    optional string quote_currency = 14;
+    // [optional] Market schedule in Pythnet format.
+    // If absent, the default schedule is used (market is always open).
+    optional string market_schedule = 15;
+}
+
+// An item of the state describing a feed.
+message Feed {
+    optional FeedMetadata metadata = 1;
+    // [optional] If present, the aggregator will activate the feed at the specified instant.
+    optional google.protobuf.Timestamp pending_activation = 2;
+    // [optional] If present, the aggregator will deactivate the feed at the specified instant.
+    optional google.protobuf.Timestamp pending_deactivation = 3;
+    // Additional state per publisher.
+    // If an eligible publisher is not listed here, the corresponding state should be considered empty.
+    repeated FeedPublisherState per_publisher = 4;
+    // TODO: list of permissioned publisher IDs.
+}
+
+// A part of the feed state related to a particular publisher.
+message FeedPublisherState {
+    // [required] Publisher ID. Restricted to uint16.
+    optional uint32 publisher_id = 1;
+    // [optional] Timestamp of the last update received from this publisher to this feed.
+    // This timestamp is provided by Nats/Kafka, not by publisher.
+    // Can be absent if no update was ever received or if the last update was deemed no longer relevant.
+    optional google.protobuf.Timestamp last_update_timestamp = 2;
+    // [optional] Publisher timestamp of the last update received from this publisher to this feed.
+    // This timestamp is provided by publisher.
+    // Can be absent if no update was ever received or if the last update was deemed no longer relevant.
+    optional google.protobuf.Timestamp last_publisher_timestamp = 3;
+    // [optional] Data of the last update received from this publisher to this feed.
+    // Can be absent if no update was ever received or if the last update was deemed no longer relevant.
+    optional FeedData last_feed_data = 4;
+}
+
+// Data provided by a publisher for a certain feed.
+message FeedData {
+    // [required] Timestamp provided by the source of data that the publisher uses (e.g. an exchange).
+    // If no such timestamp is available, it should be set to the same value as `publisher_timestamp`.
+    optional google.protobuf.Timestamp source_timestamp = 1;
+    // [required] Timestamp of the publisher.
+    optional google.protobuf.Timestamp publisher_timestamp = 2;
+    // [optional] Best executable price. Can be absent if no data is available. Never present for funding rate feeds.
+    optional int64 price = 3;
+    // [optional] Best bid price. Can be absent if no data is available. Never present for funding rate feeds.
+    optional int64 best_bid_price = 4;
+    // [optional] Best ask price. Can be absent if no data is available. Never present for funding rate feeds.
+    optional int64 best_ask_price = 5;
+    // [optional] Funding rate. Can be absent if no data is available. Can only be present for funding rate feeds.
+    optional int64 funding_rate = 6;
+}

+ 1 - 1
lazer/publisher_sdk/rust/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "pyth-lazer-publisher-sdk"
-version = "0.1.4"
+version = "0.1.5"
 edition = "2021"
 description = "Pyth Lazer Publisher SDK types."
 license = "Apache-2.0"

+ 4 - 0
lazer/publisher_sdk/rust/src/lib.rs

@@ -22,6 +22,10 @@ pub mod governance_instruction {
     pub use crate::protobuf::governance_instruction::*;
 }
 
+pub mod state {
+    pub use crate::protobuf::state::*;
+}
+
 #[allow(rustdoc::broken_intra_doc_links)]
 mod protobuf {
     include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs"));

Різницю між файлами не показано, бо вона завелика
+ 2 - 0
packages/known-publishers/src/icons/dark/amber.svg


Різницю між файлами не показано, бо вона завелика
+ 2 - 0
packages/known-publishers/src/icons/light/amber.svg


Різницю між файлами не показано, бо вона завелика
+ 1 - 0
packages/known-publishers/src/icons/monochrome/amber.svg


+ 11 - 0
packages/known-publishers/src/index.tsx

@@ -5,10 +5,13 @@ import nobiColor from "./icons/color/nobi.svg";
 import orcaColor from "./icons/color/orca.svg";
 import sentioColor from "./icons/color/sentio.svg";
 import wooColor from "./icons/color/woo.svg";
+import amberDark from "./icons/dark/amber.svg";
 import ltpDark from "./icons/dark/ltp.svg";
+import amberLight from "./icons/light/amber.svg";
 import ltpLight from "./icons/light/ltp.svg";
 import alenoMonochrome from "./icons/monochrome/aleno.svg";
 import alphanonce from "./icons/monochrome/alphanonce.svg";
+import amberMonochrome from "./icons/monochrome/amber.svg";
 import blocksize from "./icons/monochrome/blocksize.svg";
 import elfomo from "./icons/monochrome/elfomo.svg";
 import finazonMonochrome from "./icons/monochrome/finazon.svg";
@@ -109,6 +112,14 @@ export const knownPublishers = {
       monochrome: kronosResearchMonochrome,
     },
   },
+  "2ehFijXkacypZL4jdfPm38BJnMKsN2nMHm8xekbujjdx": {
+    name: "Amber Group",
+    icon: {
+      monochrome: amberMonochrome,
+      dark: amberDark,
+      light: amberLight,
+    },
+  },
 } as const;
 
 export const lookup = (value: string) =>

+ 2 - 0
pyth-lazer-agent/.dockerignore

@@ -0,0 +1,2 @@
+target
+Dockerfile

+ 6 - 0
pyth-lazer-agent/.gitignore

@@ -0,0 +1,6 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Mac OS
+.DS_Store

+ 3204 - 0
pyth-lazer-agent/Cargo.lock

@@ -0,0 +1,3204 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "arraydeque"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "backoff"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
+dependencies = [
+ "getrandom 0.2.16",
+ "instant",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
+
+[[package]]
+name = "bincode"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
+dependencies = [
+ "bincode_derive",
+ "serde",
+ "unty",
+]
+
+[[package]]
+name = "bincode_derive"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
+dependencies = [
+ "virtue",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "borsh"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
+dependencies = [
+ "borsh-derive",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "bs58"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
+
+[[package]]
+name = "cc"
+version = "1.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "clap"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
+name = "config"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
+dependencies = [
+ "async-trait",
+ "convert_case",
+ "json5",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "winnow",
+ "yaml-rust2",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.16",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
+dependencies = [
+ "byteorder",
+ "digest 0.9.0",
+ "rand_core 0.5.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest 0.10.7",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid",
+ "zeroize",
+]
+
+[[package]]
+name = "derivative"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_more"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
+name = "ed25519"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
+dependencies = [
+ "signature 1.6.4",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature 2.2.0",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
+dependencies = [
+ "curve25519-dalek 3.2.0",
+ "ed25519 1.5.3",
+ "rand 0.7.3",
+ "serde",
+ "sha2 0.9.9",
+ "zeroize",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek 4.1.3",
+ "ed25519 2.2.3",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2 0.10.9",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "five8"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75b8549488b4715defcb0d8a8a1c1c76a80661b5fa106b4ca0e7fce59d7d875"
+dependencies = [
+ "five8_core",
+]
+
+[[package]]
+name = "five8_const"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b"
+dependencies = [
+ "five8_core",
+]
+
+[[package]]
+name = "five8_core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs-err"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.3",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "humantime"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
+
+[[package]]
+name = "humantime-serde"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c"
+dependencies = [
+ "humantime",
+ "serde",
+]
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.3",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
+[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[package]]
+name = "matchers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+dependencies = [
+ "regex-automata 0.1.10",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2611b99ab098a31bdc8be48b4f1a285ca0ced28bd5b4f23e45efa8c63b09efa5"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl"
+version = "0.10.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "ordered-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pest"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.12",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "protobuf"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4"
+dependencies = [
+ "once_cell",
+ "protobuf-support",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "protobuf-codegen"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace"
+dependencies = [
+ "anyhow",
+ "once_cell",
+ "protobuf",
+ "protobuf-parse",
+ "regex",
+ "tempfile",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "protobuf-parse"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "log",
+ "protobuf",
+ "protobuf-support",
+ "tempfile",
+ "thiserror 1.0.69",
+ "which",
+]
+
+[[package]]
+name = "protobuf-support"
+version = "3.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6"
+dependencies = [
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "pyth-lazer-agent"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "backoff",
+ "bincode",
+ "clap",
+ "config",
+ "derivative",
+ "ed25519-dalek 2.1.1",
+ "futures",
+ "futures-util",
+ "http",
+ "http-body-util",
+ "humantime-serde",
+ "hyper",
+ "hyper-util",
+ "protobuf",
+ "pyth-lazer-protocol",
+ "pyth-lazer-publisher-sdk",
+ "serde",
+ "serde_json",
+ "soketto",
+ "solana-keypair",
+ "tokio",
+ "tokio-tungstenite",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+]
+
+[[package]]
+name = "pyth-lazer-protocol"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9bdf4e2ba853a8b437309487542e742c7d094d8db189db194cb538f2be02ecd"
+dependencies = [
+ "anyhow",
+ "base64 0.22.1",
+ "byteorder",
+ "derive_more",
+ "itertools",
+ "protobuf",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pyth-lazer-publisher-sdk"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e633db28ca38210de8ab3e99d5bd85ad8cae08a08bb0292506340ee9d1c718"
+dependencies = [
+ "anyhow",
+ "fs-err",
+ "humantime",
+ "protobuf",
+ "protobuf-codegen",
+ "pyth-lazer-protocol",
+ "serde-value",
+ "tracing",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata 0.4.9",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
+dependencies = [
+ "regex-syntax 0.6.29",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.8.5",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "ron"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
+dependencies = [
+ "base64 0.21.7",
+ "bitflags",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+ "trim-in-place",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.37.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand 0.8.5",
+ "rkyv",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
+dependencies = [
+ "block-buffer 0.9.0",
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.9.0",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "1.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+
+[[package]]
+name = "socket2"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "soketto"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures",
+ "http",
+ "httparse",
+ "log",
+ "rand 0.8.5",
+ "sha1",
+]
+
+[[package]]
+name = "solana-atomic-u64"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52e52720efe60465b052b9e7445a01c17550666beec855cce66f44766697bc2"
+dependencies = [
+ "parking_lot",
+]
+
+[[package]]
+name = "solana-decode-error"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "solana-define-syscall"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ae3e2abcf541c8122eafe9a625d4d194b4023c20adde1e251f94e056bb1aee2"
+
+[[package]]
+name = "solana-hash"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63"
+dependencies = [
+ "five8",
+ "js-sys",
+ "solana-atomic-u64",
+ "solana-sanitize",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "solana-instruction"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47298e2ce82876b64f71e9d13a46bc4b9056194e7f9937ad3084385befa50885"
+dependencies = [
+ "getrandom 0.2.16",
+ "js-sys",
+ "num-traits",
+ "solana-define-syscall",
+ "solana-pubkey",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "solana-keypair"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dbb7042c2e0c561afa07242b2099d55c57bd1b1da3b6476932197d84e15e3e4"
+dependencies = [
+ "bs58",
+ "ed25519-dalek 1.0.1",
+ "rand 0.7.3",
+ "solana-pubkey",
+ "solana-seed-phrase",
+ "solana-signature",
+ "solana-signer",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "solana-pubkey"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1"
+dependencies = [
+ "five8",
+ "five8_const",
+ "getrandom 0.2.16",
+ "js-sys",
+ "num-traits",
+ "solana-atomic-u64",
+ "solana-decode-error",
+ "solana-define-syscall",
+ "solana-sanitize",
+ "solana-sha256-hasher",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "solana-sanitize"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61f1bc1357b8188d9c4a3af3fc55276e56987265eb7ad073ae6f8180ee54cecf"
+
+[[package]]
+name = "solana-seed-phrase"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36187af2324f079f65a675ec22b31c24919cb4ac22c79472e85d819db9bbbc15"
+dependencies = [
+ "hmac",
+ "pbkdf2",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "solana-sha256-hasher"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea"
+dependencies = [
+ "sha2 0.10.9",
+ "solana-define-syscall",
+ "solana-hash",
+]
+
+[[package]]
+name = "solana-signature"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c8ec8e657aecfc187522fc67495142c12f35e55ddeca8698edbb738b8dbd8c"
+dependencies = [
+ "ed25519-dalek 1.0.1",
+ "five8",
+ "solana-sanitize",
+]
+
+[[package]]
+name = "solana-signer"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c41991508a4b02f021c1342ba00bcfa098630b213726ceadc7cb032e051975b"
+dependencies = [
+ "solana-pubkey",
+ "solana-signature",
+ "solana-transaction-error",
+]
+
+[[package]]
+name = "solana-transaction-error"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1"
+dependencies = [
+ "solana-instruction",
+ "solana-sanitize",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tempfile"
+version = "3.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.3",
+ "once_cell",
+ "rustix 1.0.7",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
+dependencies = [
+ "futures-util",
+ "log",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tungstenite",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "trim-in-place"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
+
+[[package]]
+name = "tungstenite"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
+dependencies = [
+ "bytes",
+ "data-encoding",
+ "http",
+ "httparse",
+ "log",
+ "native-tls",
+ "rand 0.9.1",
+ "sha1",
+ "thiserror 2.0.12",
+ "url",
+ "utf-8",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unty"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "virtue"
+version = "0.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix 0.38.44",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "yaml-rust2"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18b783b2c2789414f8bb84ca3318fc9c2d7e7be1c22907d37839a58dedb369d3"
+dependencies = [
+ "arraydeque",
+ "encoding_rs",
+ "hashlink",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]

+ 34 - 0
pyth-lazer-agent/Cargo.toml

@@ -0,0 +1,34 @@
+[package]
+name = "pyth-lazer-agent"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+pyth-lazer-publisher-sdk = "0.1.5"
+pyth-lazer-protocol = "0.7.2"
+
+anyhow = "1.0.98"
+backoff = "0.4.0"
+bincode = { version = "2.0.1", features = ["serde"] }
+clap = { version = "4.5.32", features = ["derive"] }
+config = "0.15.11"
+derivative = "2.2.0"
+ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
+futures = "0.3.31"
+futures-util = "0.3.31"
+http = "1.3.1"
+http-body-util = "0.1.3"
+humantime-serde = "1.1.1"
+hyper = { version = "1.6.0", features = ["http1", "server"] }
+hyper-util = { version = "0.1.10", features = ["tokio"] }
+protobuf = "3.7.2"
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
+soketto = { version = "0.8.1", features = ["http"] }
+solana-keypair = "2.2.1"
+tokio = { version = "1.44.1", features = ["full"] }
+tokio-tungstenite = { version = "0.26.2", features = ["native-tls", "url"] }
+tokio-util = { version = "0.7.14", features = ["compat"] }
+tracing = "0.1.41"
+tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
+url = { version = "2.5.4", features = ["serde"] }

+ 17 - 0
pyth-lazer-agent/Dockerfile

@@ -0,0 +1,17 @@
+FROM rust:slim-bookworm AS builder
+
+RUN apt update && apt install -y curl libssl-dev pkg-config build-essential && apt clean all
+
+ADD . /pyth-lazer-agent
+WORKDIR /pyth-lazer-agent
+
+RUN cargo build --release
+
+FROM debian:12-slim
+
+RUN apt update && apt install -y libssl-dev && apt clean all
+
+COPY --from=builder /pyth-lazer-agent/target/release/pyth-lazer-agent /pyth-lazer-agent/
+COPY --from=builder /pyth-lazer-agent/config/* /pyth-lazer-agent/config/
+
+ENTRYPOINT ["/pyth-lazer-agent/pyth-lazer-agent"]

+ 5 - 0
pyth-lazer-agent/config/config.toml

@@ -0,0 +1,5 @@
+relayer_urls = ["ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction", "ws://relayer-0.pyth-lazer.dourolabs.app/v1/transaction"]
+authorization_token = "token1"
+publish_keypair_path = "/path/to/solana/id.json"
+listen_address = "0.0.0.0:1776"
+publish_interval_duration = "0.5ms"

+ 4 - 0
pyth-lazer-agent/rust-toolchain.toml

@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.87.0"
+profile = "minimal"
+components = ["rustfmt", "clippy"]

+ 37 - 0
pyth-lazer-agent/src/config.rs

@@ -0,0 +1,37 @@
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::time::Duration;
+
+use config::{Environment, File};
+use derivative::Derivative;
+use serde::Deserialize;
+use url::Url;
+
+#[derive(Deserialize, Derivative, Clone, PartialEq)]
+#[derivative(Debug)]
+pub struct Config {
+    pub listen_address: SocketAddr,
+    pub relayer_urls: Vec<Url>,
+    #[derivative(Debug = "ignore")]
+    pub authorization_token: String,
+    #[derivative(Debug = "ignore")]
+    pub publish_keypair_path: PathBuf,
+    #[serde(with = "humantime_serde", default = "default_publish_interval")]
+    pub publish_interval_duration: Duration,
+}
+
+fn default_publish_interval() -> Duration {
+    Duration::from_micros(500)
+}
+
+pub fn load_config(config_path: String) -> anyhow::Result<Config> {
+    let config = config::Config::builder()
+        .add_source(File::with_name(&config_path))
+        .add_source(Environment::with_prefix("LAZER_AGENT").separator("__"))
+        .build()?
+        .try_deserialize()?;
+    Ok(config)
+}
+
+// Default capacity for all tokio mpsc channels that communicate between tasks.
+pub const CHANNEL_CAPACITY: usize = 1000;

+ 120 - 0
pyth-lazer-agent/src/http_server.rs

@@ -0,0 +1,120 @@
+use anyhow::{Context, Result};
+use hyper::{Response, StatusCode, body::Bytes, server::conn::http1, service::service_fn};
+use hyper_util::rt::TokioIo;
+use soketto::{
+    BoxedError,
+    handshake::http::{Server, is_upgrade_request},
+};
+use std::{io, net::SocketAddr};
+use tokio::net::{TcpListener, TcpStream};
+use tracing::{debug, info, instrument, warn};
+
+use crate::{
+    config::Config,
+    lazer_publisher::LazerPublisher,
+    publisher_handle::{PublisherConnectionContext, handle_publisher},
+};
+
+type FullBody = http_body_util::Full<Bytes>;
+
+#[derive(Debug)]
+pub enum Request {
+    PublisherV1,
+    PublisherV2,
+}
+
+pub struct RelayerRequest(pub http::Request<hyper::body::Incoming>);
+
+const PUBLISHER_WS_URI: &str = "/v1/publisher";
+const PUBLISHER_WS_URI_V2: &str = "/v2/publisher";
+
+pub async fn run(config: Config, lazer_publisher: LazerPublisher) -> Result<()> {
+    let listener = TcpListener::bind(&config.listen_address).await?;
+    info!("listening on {:?}", &config.listen_address);
+
+    loop {
+        let stream_addr = listener.accept().await;
+        let lazer_publisher_clone = lazer_publisher.clone();
+        tokio::spawn(async {
+            if let Err(err) = try_handle_connection(stream_addr, lazer_publisher_clone).await {
+                warn!("error while handling connection: {err:?}");
+            }
+        });
+    }
+}
+
+async fn try_handle_connection(
+    stream_addr: io::Result<(TcpStream, SocketAddr)>,
+    lazer_publisher: LazerPublisher,
+) -> Result<()> {
+    let (stream, remote_addr) = stream_addr?;
+    debug!("accepted connection from {}", remote_addr);
+    stream.set_nodelay(true)?;
+    http1::Builder::new()
+        .serve_connection(
+            TokioIo::new(stream),
+            service_fn(move |r| {
+                let request = RelayerRequest(r);
+                request_handler(request, remote_addr, lazer_publisher.clone())
+            }),
+        )
+        .with_upgrades()
+        .await?;
+    Ok(())
+}
+
+#[instrument(skip_all, fields(component = "http_server", remote_addr = remote_addr.to_string()))]
+async fn request_handler(
+    request: RelayerRequest,
+    remote_addr: SocketAddr,
+    lazer_publisher: LazerPublisher,
+) -> Result<Response<FullBody>, BoxedError> {
+    let path = request.0.uri().path();
+
+    let request_type = match path {
+        PUBLISHER_WS_URI => Request::PublisherV1,
+        PUBLISHER_WS_URI_V2 => Request::PublisherV2,
+        _ => {
+            return Ok(Response::builder()
+                .status(StatusCode::NOT_FOUND)
+                .body(FullBody::from("not found"))
+                .context("builder failed")?);
+        }
+    };
+
+    if !is_upgrade_request(&request.0) {
+        return Ok(Response::builder()
+            .status(StatusCode::BAD_REQUEST)
+            .body(FullBody::from("bad request"))
+            .context("builder failed")?);
+    }
+
+    let mut server = Server::new();
+    match server.receive_request(&request.0) {
+        Ok(response) => {
+            info!("accepted connection from publisher");
+            match request_type {
+                Request::PublisherV1 | Request::PublisherV2 => {
+                    let publisher_connection_context = PublisherConnectionContext {
+                        request_type,
+                        _remote_addr: remote_addr,
+                    };
+                    tokio::spawn(handle_publisher(
+                        server,
+                        request.0,
+                        publisher_connection_context,
+                        lazer_publisher,
+                    ));
+                    Ok(response.map(|()| FullBody::default()))
+                }
+            }
+        }
+        Err(e) => {
+            warn!("Could not upgrade connection: {}", e);
+            Ok(Response::builder()
+                .status(StatusCode::INTERNAL_SERVER_ERROR)
+                .body(FullBody::from("internal server error"))
+                .context("builder failed")?)
+        }
+    }
+}

+ 150 - 0
pyth-lazer-agent/src/lazer_publisher.rs

@@ -0,0 +1,150 @@
+use crate::config::{CHANNEL_CAPACITY, Config};
+use crate::relayer_session::RelayerSender;
+use anyhow::{Context, Result, bail};
+use ed25519_dalek::{Signer, SigningKey};
+use protobuf::well_known_types::timestamp::Timestamp;
+use protobuf::{Message, MessageField};
+use pyth_lazer_publisher_sdk::publisher_update::{FeedUpdate, PublisherUpdate};
+use pyth_lazer_publisher_sdk::transaction::lazer_transaction::Payload;
+use pyth_lazer_publisher_sdk::transaction::signature_data::Data::Ed25519;
+use pyth_lazer_publisher_sdk::transaction::{
+    Ed25519SignatureData, LazerTransaction, SignatureData, SignedLazerTransaction,
+};
+use solana_keypair::read_keypair_file;
+use tokio::{
+    select,
+    sync::mpsc::{self, Receiver, Sender},
+    time::interval,
+};
+use tracing::error;
+
+#[derive(Clone)]
+pub struct LazerPublisher {
+    sender: Sender<FeedUpdate>,
+}
+
+impl LazerPublisher {
+    pub async fn new(config: &Config) -> Self {
+        let relayer_senders = futures::future::join_all(
+            config
+                .relayer_urls
+                .iter()
+                .map(async |url| RelayerSender::new(url, &config.authorization_token).await),
+        )
+        .await;
+
+        let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
+        let mut task = LazerPublisherTask {
+            config: config.clone(),
+            receiver,
+            pending_updates: Vec::new(),
+            relayer_senders,
+        };
+        tokio::spawn(async move { task.run().await });
+        Self { sender }
+    }
+
+    pub async fn push_feed_update(&self, feed_update: FeedUpdate) -> Result<()> {
+        self.sender.send(feed_update).await?;
+        Ok(())
+    }
+}
+
+struct LazerPublisherTask {
+    // connection state
+    config: Config,
+    receiver: Receiver<FeedUpdate>,
+    pending_updates: Vec<FeedUpdate>,
+    relayer_senders: Vec<RelayerSender>,
+}
+
+impl LazerPublisherTask {
+    fn load_signing_key(&self) -> Result<SigningKey> {
+        // Read the keypair from the file using Solana SDK because it's the same key used by the Pythnet publisher
+        let publish_keypair = match read_keypair_file(&self.config.publish_keypair_path) {
+            Ok(k) => k,
+            Err(e) => {
+                tracing::error!(
+                    error = ?e,
+                    publish_keypair_path = self.config.publish_keypair_path.display().to_string(),
+                    "Reading publish keypair returned an error. ",
+                );
+                bail!("Reading publish keypair returned an error.");
+            }
+        };
+
+        SigningKey::from_keypair_bytes(&publish_keypair.to_bytes())
+            .context("Failed to create signing key from keypair")
+    }
+
+    pub async fn run(&mut self) {
+        let signing_key = match self.load_signing_key() {
+            Ok(signing_key) => signing_key,
+            Err(e) => {
+                tracing::error!("Failed to load signing key: {e:?}");
+                // Can't proceed on key failure
+                panic!("Failed to load signing key: {e:?}");
+            }
+        };
+
+        let mut publish_interval = interval(self.config.publish_interval_duration);
+        loop {
+            select! {
+                Some(feed_update) = self.receiver.recv() => {
+                    self.pending_updates.push(feed_update);
+                }
+                _ = publish_interval.tick() => {
+                    if let Err(err) = self.batch_transaction(&signing_key).await {
+                        error!("Failed to publish updates: {}", err);
+                    }
+                }
+            }
+        }
+    }
+
+    async fn batch_transaction(&mut self, signing_key: &SigningKey) -> Result<()> {
+        if self.pending_updates.is_empty() {
+            return Ok(());
+        }
+
+        let publisher_update = PublisherUpdate {
+            updates: self.pending_updates.clone(),
+            publisher_timestamp: MessageField::some(Timestamp::now()),
+            special_fields: Default::default(),
+        };
+        let lazer_transaction = LazerTransaction {
+            payload: Some(Payload::PublisherUpdate(publisher_update)),
+            special_fields: Default::default(),
+        };
+        let buf = match lazer_transaction.write_to_bytes() {
+            Ok(buf) => buf,
+            Err(e) => {
+                tracing::warn!("Failed to encode Lazer transaction to bytes: {:?}", e);
+                bail!("Failed to encode Lazer transaction")
+            }
+        };
+        let signature = signing_key.sign(&buf);
+        let signature_data = SignatureData {
+            data: Some(Ed25519(Ed25519SignatureData {
+                signature: Some(signature.to_bytes().into()),
+                public_key: Some(signing_key.verifying_key().to_bytes().into()),
+                special_fields: Default::default(),
+            })),
+            special_fields: Default::default(),
+        };
+        let signed_lazer_transaction = SignedLazerTransaction {
+            signature_data: MessageField::some(signature_data),
+            payload: Some(buf),
+            special_fields: Default::default(),
+        };
+        futures::future::join_all(
+            self.relayer_senders
+                .iter_mut()
+                .map(|relayer_sender| relayer_sender.sender.send(signed_lazer_transaction.clone())),
+        )
+        .await;
+
+        self.pending_updates.clear();
+        Ok(())
+    }
+}

+ 46 - 0
pyth-lazer-agent/src/main.rs

@@ -0,0 +1,46 @@
+use {
+    crate::lazer_publisher::LazerPublisher,
+    anyhow::Context,
+    clap::Parser,
+    tracing::{info, level_filters::LevelFilter},
+    tracing_subscriber::{EnvFilter, fmt::format::FmtSpan},
+};
+
+mod config;
+mod http_server;
+mod lazer_publisher;
+mod publisher_handle;
+mod relayer_session;
+mod websocket_utils;
+
+#[derive(Parser)]
+#[command(version)]
+struct Cli {
+    #[clap(short, long, default_value = "config.toml")]
+    config: String,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+    tracing_subscriber::fmt()
+        .with_env_filter(
+            EnvFilter::builder()
+                .with_default_directive(LevelFilter::INFO.into())
+                .from_env()
+                .expect("invalid RUST_LOG env var"),
+        )
+        .with_span_events(FmtSpan::NONE)
+        .json()
+        .with_span_list(false)
+        .init();
+
+    let args = Cli::parse();
+    let config =
+        config::load_config(args.config.to_string()).context("Failed to read config file")?;
+    info!(?config, "starting lazer-agent");
+
+    let lazer_publisher = LazerPublisher::new(&config).await;
+    http_server::run(config, lazer_publisher).await?;
+
+    Ok(())
+}

+ 173 - 0
pyth-lazer-agent/src/publisher_handle.rs

@@ -0,0 +1,173 @@
+use std::net::SocketAddr;
+
+use anyhow::bail;
+use futures_util::io::{BufReader, BufWriter};
+use hyper_util::rt::TokioIo;
+use protobuf::MessageField;
+use protobuf::well_known_types::timestamp::Timestamp;
+use pyth_lazer_protocol::publisher::{
+    PriceFeedDataV1, PriceFeedDataV2, ServerResponse, UpdateDeserializationErrorResponse,
+};
+use pyth_lazer_publisher_sdk::publisher_update::feed_update::Update;
+use pyth_lazer_publisher_sdk::publisher_update::{FeedUpdate, FundingRateUpdate, PriceUpdate};
+use soketto::handshake::http::Server;
+use tokio::{pin, select};
+use tokio_util::compat::TokioAsyncReadCompatExt;
+use tracing::{error, instrument, warn};
+
+use crate::{
+    http_server,
+    lazer_publisher::LazerPublisher,
+    websocket_utils::{handle_websocket_error, send_text},
+};
+
+pub struct PublisherConnectionContext {
+    pub request_type: http_server::Request,
+    pub _remote_addr: SocketAddr,
+}
+
+#[instrument(
+    skip(server, request, lazer_publisher, context),
+    fields(component = "publisher_ws")
+)]
+pub async fn handle_publisher(
+    server: Server,
+    request: hyper::Request<hyper::body::Incoming>,
+    context: PublisherConnectionContext,
+    lazer_publisher: LazerPublisher,
+) {
+    if let Err(err) = try_handle_publisher(server, request, context, lazer_publisher).await {
+        handle_websocket_error(err);
+    }
+}
+
+#[instrument(
+    skip(server, request, lazer_publisher, context),
+    fields(component = "publisher_ws")
+)]
+async fn try_handle_publisher(
+    server: Server,
+    request: hyper::Request<hyper::body::Incoming>,
+    context: PublisherConnectionContext,
+    lazer_publisher: LazerPublisher,
+) -> anyhow::Result<()> {
+    let stream = hyper::upgrade::on(request).await?;
+    let io = TokioIo::new(stream);
+    let stream = BufReader::new(BufWriter::new(io.compat()));
+    let (mut ws_sender, mut ws_receiver) = server.into_builder(stream).finish();
+
+    let mut receive_buf = Vec::new();
+
+    let mut error_count = 0u32;
+    const MAX_ERROR_LOG: u32 = 10u32;
+    const MAX_ERROR_DISCONNECT: u32 = 100u32;
+
+    loop {
+        receive_buf.clear();
+        {
+            // soketto is not cancel-safe, so we need to store the future and poll it
+            // in the inner loop.
+            let receive = async { ws_receiver.receive(&mut receive_buf).await };
+            pin!(receive);
+            #[allow(clippy::never_loop)] // false positive
+            loop {
+                select! {
+                    _result = &mut receive => {
+                        break
+                    }
+                }
+            }
+        }
+
+        // reply with an error if we can't parse the binary update
+        let feed_update: FeedUpdate = match context.request_type {
+            http_server::Request::PublisherV1 => {
+                match bincode::serde::decode_from_slice::<PriceFeedDataV1, _>(
+                    &receive_buf,
+                    bincode::config::legacy(),
+                ) {
+                    Ok((data, _)) => {
+                        let source_timestamp = MessageField::some(Timestamp {
+                            seconds: (data.source_timestamp_us.0 / 1_000_000) as i64,
+                            nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32,
+                            special_fields: Default::default(),
+                        });
+                        FeedUpdate {
+                            feed_id: Some(data.price_feed_id.0),
+                            source_timestamp,
+                            update: Some(Update::PriceUpdate(PriceUpdate {
+                                price: data.price.map(|p| p.0.get()),
+                                best_bid_price: data.best_bid_price.map(|p| p.0.get()),
+                                best_ask_price: data.best_ask_price.map(|p| p.0.get()),
+                                ..PriceUpdate::default()
+                            })),
+                            special_fields: Default::default(),
+                        }
+                    }
+                    Err(err) => {
+                        error_count += 1;
+                        if error_count <= MAX_ERROR_LOG {
+                            warn!("Error decoding v1 update error: {:?}", err);
+                        }
+                        if error_count >= MAX_ERROR_DISCONNECT {
+                            error!("Error threshold reached; disconnecting",);
+                            bail!("Error threshold reached");
+                        }
+                        let error_json = &serde_json::to_string::<ServerResponse>(
+                            &UpdateDeserializationErrorResponse {
+                                error: format!("failed to parse binary update: {err}"),
+                            }
+                            .into(),
+                        )?;
+                        send_text(&mut ws_sender, error_json).await?;
+                        continue;
+                    }
+                }
+            }
+            http_server::Request::PublisherV2 => {
+                match bincode::serde::decode_from_slice::<PriceFeedDataV2, _>(
+                    &receive_buf,
+                    bincode::config::legacy(),
+                ) {
+                    Ok((data, _)) => {
+                        let source_timestamp = MessageField::some(Timestamp {
+                            seconds: (data.source_timestamp_us.0 / 1_000_000) as i64,
+                            nanos: (data.source_timestamp_us.0 % 1_000_000 * 1000) as i32,
+                            special_fields: Default::default(),
+                        });
+                        FeedUpdate {
+                            feed_id: Some(data.price_feed_id.0),
+                            source_timestamp,
+                            update: Some(Update::FundingRateUpdate(FundingRateUpdate {
+                                price: data.price.map(|p| p.0.get()),
+                                rate: data.funding_rate.map(|r| r.0),
+                                ..FundingRateUpdate::default()
+                            })),
+                            special_fields: Default::default(),
+                        }
+                    }
+                    Err(err) => {
+                        error_count += 1;
+                        if error_count <= MAX_ERROR_LOG {
+                            warn!("Error decoding v2 update error: {:?}", err);
+                        }
+                        if error_count >= MAX_ERROR_DISCONNECT {
+                            error!("Error threshold reached; disconnecting");
+                            bail!("Error threshold reached");
+                        }
+                        let error_json = &serde_json::to_string::<ServerResponse>(
+                            &UpdateDeserializationErrorResponse {
+                                error: format!("failed to parse binary update: {err}"),
+                            }
+                            .into(),
+                        )?;
+                        send_text(&mut ws_sender, error_json).await?;
+                        continue;
+                    }
+                }
+            }
+        };
+
+        lazer_publisher.push_feed_update(feed_update).await?;
+    }
+}

+ 157 - 0
pyth-lazer-agent/src/relayer_session.rs

@@ -0,0 +1,157 @@
+use crate::config::CHANNEL_CAPACITY;
+use anyhow::{Result, bail};
+use backoff::ExponentialBackoffBuilder;
+use backoff::backoff::Backoff;
+use futures_util::stream::{SplitSink, SplitStream};
+use futures_util::{SinkExt, StreamExt};
+use http::HeaderValue;
+use protobuf::Message;
+use pyth_lazer_publisher_sdk::transaction::SignedLazerTransaction;
+use std::time::Duration;
+use tokio::net::TcpStream;
+use tokio::{
+    select,
+    sync::mpsc::{self, Receiver, Sender},
+};
+use tokio_tungstenite::tungstenite::client::IntoClientRequest;
+use tokio_tungstenite::{
+    MaybeTlsStream, WebSocketStream, connect_async_with_config,
+    tungstenite::Message as TungsteniteMessage,
+};
+use url::Url;
+
+pub struct RelayerSender {
+    pub(crate) sender: Sender<SignedLazerTransaction>,
+}
+
+impl RelayerSender {
+    pub async fn new(url: &Url, token: &str) -> Self {
+        let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
+        let mut task = RelayerSessionTask {
+            url: url.clone(),
+            token: token.to_owned(),
+            receiver,
+        };
+        tokio::spawn(async move { task.run().await });
+        Self { sender }
+    }
+}
+
+type RelayerWsSender = SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, TungsteniteMessage>;
+type RelayerWsReceiver = SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>;
+
+async fn connect_to_relayer(
+    mut url: Url,
+    token: &str,
+) -> Result<(RelayerWsSender, RelayerWsReceiver)> {
+    tracing::info!("connecting to the relayer at {}", url);
+    url.set_path("/v1/transaction");
+    let mut req = url.clone().into_client_request()?;
+    let headers = req.headers_mut();
+    headers.insert(
+        "Authorization",
+        HeaderValue::from_str(&format!("Bearer {token}"))?,
+    );
+    let (ws_stream, _) = connect_async_with_config(req, None, true).await?;
+    Ok(ws_stream.split())
+}
+
+struct RelayerWsSession {
+    ws_sender: RelayerWsSender,
+}
+
+impl RelayerWsSession {
+    async fn send_transaction(
+        &mut self,
+        signed_lazer_transaction: SignedLazerTransaction,
+    ) -> Result<()> {
+        tracing::debug!(
+            "Sending SignedLazerTransaction: {:?}",
+            signed_lazer_transaction
+        );
+        let buf = signed_lazer_transaction.write_to_bytes()?;
+        self.ws_sender
+            .send(TungsteniteMessage::from(buf.clone()))
+            .await?;
+        self.ws_sender.flush().await?;
+        Ok(())
+    }
+}
+
+struct RelayerSessionTask {
+    // connection state
+    url: Url,
+    token: String,
+    receiver: Receiver<SignedLazerTransaction>,
+}
+
+impl RelayerSessionTask {
+    pub async fn run(&mut self) {
+        let initial_interval = Duration::from_millis(100);
+        let max_interval = Duration::from_secs(5);
+        let mut backoff = ExponentialBackoffBuilder::new()
+            .with_initial_interval(initial_interval)
+            .with_max_interval(max_interval)
+            .with_max_elapsed_time(None)
+            .build();
+
+        let mut failure_count = 0;
+
+        loop {
+            match self.run_relayer_connection().await {
+                Ok(()) => {
+                    tracing::info!("relayer session graceful shutdown");
+                    return;
+                }
+                Err(e) => {
+                    failure_count += 1;
+                    let next_backoff = backoff.next_backoff().unwrap_or(max_interval);
+                    tracing::error!(
+                        "relayer session failed with error: {:?}, failure_count: {}; retrying in {:?}",
+                        e,
+                        failure_count,
+                        next_backoff
+                    );
+                    tokio::time::sleep(next_backoff).await;
+                }
+            }
+        }
+    }
+
+    pub async fn run_relayer_connection(&mut self) -> Result<()> {
+        // Establish relayer connection
+        // Relayer will drop the connection if no data received in 5s
+        let (relayer_ws_sender, mut relayer_ws_receiver) =
+            connect_to_relayer(self.url.clone(), &self.token).await?;
+        let mut relayer_ws_session = RelayerWsSession {
+            ws_sender: relayer_ws_sender,
+        };
+
+        loop {
+            select! {
+                Some(transaction) = self.receiver.recv() => {
+                    if let Err(e) = relayer_ws_session.send_transaction(transaction).await
+                    {
+                        tracing::error!("Error publishing transaction to Lazer relayer: {e:?}");
+                        bail!("Failed to publish transaction to Lazer relayer: {e:?}");
+                    }
+                }
+                // Handle messages from the relayers, such as errors if we send a bad update
+                msg = relayer_ws_receiver.next() => {
+                    match msg {
+                        Some(Ok(msg)) => {
+                            tracing::debug!("Received message from relayer: {msg:?}");
+                        }
+                        Some(Err(e)) => {
+                            tracing::error!("Error receiving message from at relayer: {e:?}");
+                        }
+                        None => {
+                            tracing::error!("relayer connection closed");
+                            bail!("relayer connection closed");
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 43 - 0
pyth-lazer-agent/src/websocket_utils.rs

@@ -0,0 +1,43 @@
+use std::time::Duration;
+
+use anyhow::Error;
+use futures::{AsyncRead, AsyncWrite};
+use soketto::Sender;
+use tokio::time::timeout;
+use tracing::{debug, warn};
+
+const SEND_TIMEOUT: Duration = Duration::from_secs(10);
+
+pub fn handle_websocket_error(err: Error) {
+    if let Some(soketto_err) = err.downcast_ref::<soketto::connection::Error>() {
+        match soketto_err {
+            soketto::connection::Error::Closed => {
+                debug!("connection to client was closed")
+            }
+            soketto::connection::Error::Io(soketto_io_err) => {
+                if soketto_io_err.kind() == std::io::ErrorKind::ConnectionReset {
+                    debug!("Client disconnected WebSocket connection");
+                } else {
+                    warn!("Websocket IO error: {:?}", soketto_io_err);
+                }
+            }
+            _ => {
+                warn!("error while handling connection: {:?}", err.to_string())
+            }
+        }
+    } else {
+        warn!("error while handling connection: {:?}", err.to_string());
+    }
+}
+
+pub async fn send_text<T: AsyncRead + AsyncWrite + Unpin>(
+    sender: &mut Sender<T>,
+    text: &str,
+) -> anyhow::Result<()> {
+    timeout(SEND_TIMEOUT, async {
+        sender.send_text(text).await?;
+        sender.flush().await?;
+        anyhow::Ok(())
+    })
+    .await?
+}

Деякі файли не було показано, через те що забагато файлів було змінено