Merge branch 'main' of https://github.com/hyperlane-xyz/hyperlane-monorepo into ltyu/zerion-configs

ltyu/zerion-configs
Le Yu 2 days ago
commit 2cbc553ded
  1. 5
      .changeset/mighty-panthers-play.md
  2. 5
      .changeset/odd-frogs-move.md
  3. 7
      .changeset/real-phones-bake.md
  4. 6
      .changeset/strange-poems-build.md
  5. 26
      .github/workflows/test.yml
  6. 2
      .registryrc
  7. 2
      rust/main/agents/relayer/src/msg/pending_message.rs
  8. 35
      rust/main/agents/scraper/src/agent.rs
  9. 30
      rust/main/agents/scraper/src/db/message.rs
  10. 5
      rust/main/agents/scraper/src/main.rs
  11. 6
      rust/main/agents/scraper/src/store.rs
  12. 43
      rust/main/agents/scraper/src/store/deliveries.rs
  13. 64
      rust/main/agents/scraper/src/store/dispatches.rs
  14. 43
      rust/main/agents/scraper/src/store/payments.rs
  15. 164
      rust/main/agents/scraper/src/store/storage.rs
  16. 28
      rust/main/chains/hyperlane-ethereum/src/contracts/mailbox.rs
  17. 8
      rust/main/chains/hyperlane-ethereum/src/contracts/validator_announce.rs
  18. 31
      rust/main/chains/hyperlane-ethereum/src/tx.rs
  19. 64
      rust/main/chains/hyperlane-sealevel/src/interchain_gas.rs
  20. 92
      rust/main/chains/hyperlane-sealevel/src/mailbox.rs
  21. 5
      rust/main/chains/hyperlane-sealevel/src/merkle_tree_hook.rs
  22. 24
      rust/main/chains/hyperlane-sealevel/src/rpc/client.rs
  23. 8
      rust/main/config/mainnet_config.json
  24. 1
      rust/main/helm/hyperlane-agent/templates/relayer-statefulset.yaml
  25. 8
      rust/main/hyperlane-base/src/contract_sync/cursors/rate_limited.rs
  26. 14
      rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/backward.rs
  27. 14
      rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/forward.rs
  28. 6
      rust/main/hyperlane-base/src/contract_sync/cursors/sequence_aware/mod.rs
  29. 27
      rust/main/hyperlane-base/src/contract_sync/mod.rs
  30. 26
      rust/main/hyperlane-base/src/settings/base.rs
  31. 4
      rust/main/hyperlane-core/src/traits/db.rs
  32. 7
      rust/main/hyperlane-core/src/types/message.rs
  33. 7
      rust/sealevel/programs/hyperlane-sealevel-token/src/processor.rs
  34. 7
      solidity/CHANGELOG.md
  35. 2
      solidity/contracts/PackageVersioned.sol
  36. 4
      solidity/package.json
  37. 2
      typescript/ccip-server/CHANGELOG.md
  38. 2
      typescript/ccip-server/package.json
  39. 1
      typescript/cli/.mocharc-e2e.json
  40. 18
      typescript/cli/CHANGELOG.md
  41. 8
      typescript/cli/package.json
  42. 7
      typescript/cli/scripts/run-e2e-test.sh
  43. 2
      typescript/cli/src/version.ts
  44. 2
      typescript/github-proxy/CHANGELOG.md
  45. 2
      typescript/github-proxy/package.json
  46. 13
      typescript/helloworld/CHANGELOG.md
  47. 8
      typescript/helloworld/package.json
  48. 19
      typescript/infra/CHANGELOG.md
  49. 32
      typescript/infra/config/environments/mainnet3/agent.ts
  50. 38
      typescript/infra/config/environments/mainnet3/misc-artifacts/aave-sender-addresses.json
  51. 0
      typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-erc20-addresses.json
  52. 0
      typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-eth-addresses.json
  53. 0
      typescript/infra/config/environments/mainnet3/misc-artifacts/merkly-nft-addresses.json
  54. 8
      typescript/infra/config/environments/mainnet3/misc-artifacts/velo-message-module-addresses.json
  55. 8
      typescript/infra/config/environments/mainnet3/misc-artifacts/velo-token-bridge-addresses.json
  56. 1
      typescript/infra/config/environments/mainnet3/warp/warpIds.ts
  57. 2
      typescript/infra/helm/warp-routes/templates/_helpers.tpl
  58. 10
      typescript/infra/package.json
  59. 12
      typescript/infra/scripts/agent-utils.ts
  60. 10
      typescript/infra/scripts/agents/utils.ts
  61. 5
      typescript/infra/scripts/keys/create-keys.ts
  62. 5
      typescript/infra/scripts/keys/delete-keys.ts
  63. 7
      typescript/infra/scripts/keys/get-key-addresses.ts
  64. 38
      typescript/infra/scripts/keys/get-key.ts
  65. 9
      typescript/infra/scripts/keys/get-owner-ica.ts
  66. 2
      typescript/infra/scripts/keys/rotate-key.ts
  67. 2
      typescript/infra/scripts/keys/update-key.ts
  68. 17
      typescript/infra/scripts/warp-routes/deploy-warp-monitor.ts
  69. 22
      typescript/infra/scripts/warp-routes/monitor/metrics.ts
  70. 17
      typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts
  71. 12
      typescript/infra/src/config/agent/relayer.ts
  72. 2
      typescript/infra/src/warp/helm.ts
  73. 17
      typescript/sdk/CHANGELOG.md
  74. 9
      typescript/sdk/package.json
  75. 12
      typescript/sdk/src/consts/multisigIsm.ts
  76. 9
      typescript/sdk/src/index.ts
  77. 98
      typescript/sdk/src/metadata/chainMetadataConversion.ts
  78. 6
      typescript/sdk/src/token/Token.ts
  79. 19
      typescript/sdk/src/token/adapters/EvmTokenAdapter.ts
  80. 16
      typescript/sdk/src/utils/schemas.ts
  81. 29
      typescript/sdk/src/utils/viem.ts
  82. 6
      typescript/utils/CHANGELOG.md
  83. 2
      typescript/utils/package.json
  84. 2
      typescript/utils/src/index.ts
  85. 32
      typescript/utils/src/url.test.ts
  86. 16
      typescript/utils/src/url.ts
  87. 12
      typescript/widgets/.eslintrc
  88. 5
      typescript/widgets/.storybook/main.ts
  89. 25
      typescript/widgets/CHANGELOG.md
  90. 26
      typescript/widgets/package.json
  91. 3
      typescript/widgets/src/chains/ChainAddMenu.tsx
  92. 5
      typescript/widgets/src/chains/ChainLogo.tsx
  93. 4
      typescript/widgets/src/chains/ChainSearchMenu.tsx
  94. 48
      typescript/widgets/src/components/ErrorBoundary.tsx
  95. 6
      typescript/widgets/src/components/SearchMenu.tsx
  96. 38
      typescript/widgets/src/icons/Ellipsis.tsx
  97. 18
      typescript/widgets/src/icons/Error.tsx
  98. 29
      typescript/widgets/src/icons/Logout.tsx
  99. 30
      typescript/widgets/src/icons/Warning.tsx
  100. 4
      typescript/widgets/src/icons/Web.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,5 +0,0 @@
---
'@hyperlane-xyz/sdk': minor
---
Enroll new validators for alephzeroevmmainnet, chilizmainnet, flowmainnet, immutablezkevmmainnet, metal, polynomialfi, rarichain, rootstockmainnet, superpositionmainnet, flame, prom, inevm.

@ -1,5 +0,0 @@
---
'@hyperlane-xyz/sdk': minor
---
Added helpers to Token and token adapters to get bridged supply of tokens"

@ -1,7 +0,0 @@
---
'@hyperlane-xyz/infra': minor
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Implements persistent relayer for use in CLI

@ -1,6 +0,0 @@
---
'@hyperlane-xyz/widgets': minor
---
Props and style update: IconButton and Tooltip
New Icons: XCircleIcon and SwapIcon

@ -109,9 +109,18 @@ jobs:
- name: Unit Tests
run: yarn test:ci
cli-e2e:
cli-e2e-matrix:
runs-on: ubuntu-latest
needs: [yarn-install]
strategy:
fail-fast: false
matrix:
test:
- core
- relay
- warp-read
- warp-apply
- warp-deploy
steps:
- uses: actions/checkout@v4
with:
@ -130,8 +139,21 @@ jobs:
- name: Checkout registry
uses: ./.github/actions/checkout-registry
- name: CLI e2e tests
- name: CLI e2e tests (${{ matrix.test }})
run: yarn --cwd typescript/cli test:e2e
env:
CLI_E2E_TEST: ${{ matrix.test }}
cli-e2e:
runs-on: ubuntu-latest
needs: cli-e2e-matrix
if: always()
steps:
- name: Check cli-e2e matrix status
if: ${{ needs.cli-e2e-matrix.result != 'success' }}
run: |
echo "CLI E2E tests failed"
exit 1
agent-configs:
runs-on: ubuntu-latest

@ -1 +1 @@
3e366eae1d49da4270695b157d7af00bb761156a
82013508db45dcd55b44d2721414d26817686c8f

@ -468,7 +468,7 @@ impl PendingOperation for PendingMessage {
actual_gas_for_message = ?gas_used_by_operation,
message_gas_estimate = ?operation_estimate,
submission_gas_estimate = ?submission_estimated_cost,
message = ?self.message,
hyp_message = ?self.message,
"Gas used by message submission"
);
}

@ -3,16 +3,17 @@ use std::{collections::HashMap, sync::Arc};
use async_trait::async_trait;
use derive_more::AsRef;
use futures::future::try_join_all;
use hyperlane_core::{Delivery, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, H512};
use tokio::{sync::mpsc::Receiver as MpscReceiver, task::JoinHandle};
use tracing::{info_span, instrument::Instrumented, trace, Instrument};
use hyperlane_base::{
broadcast::BroadcastMpscSender, metrics::AgentMetrics, settings::IndexSettings, AgentMetadata,
BaseAgent, ChainMetrics, ContractSyncMetrics, ContractSyncer, CoreMetrics, HyperlaneAgentCore,
MetricsUpdater, SyncOptions,
};
use hyperlane_core::{Delivery, HyperlaneDomain, HyperlaneMessage, InterchainGasPayment, H512};
use tokio::{sync::mpsc::Receiver as MpscReceiver, task::JoinHandle};
use tracing::{info_span, instrument::Instrumented, trace, Instrument};
use crate::{chain_scraper::HyperlaneSqlDb, db::ScraperDb, settings::ScraperSettings};
use crate::{db::ScraperDb, settings::ScraperSettings, store::HyperlaneDbStore};
/// A message explorer scraper agent
#[derive(Debug, AsRef)]
@ -31,7 +32,7 @@ pub struct Scraper {
#[derive(Debug)]
struct ChainScraper {
index_settings: IndexSettings,
db: HyperlaneSqlDb,
store: HyperlaneDbStore,
domain: HyperlaneDomain,
}
@ -59,7 +60,7 @@ impl BaseAgent for Scraper {
for domain in settings.chains_to_scrape.iter() {
let chain_setup = settings.chain_setup(domain).expect("Missing chain config");
let db = HyperlaneSqlDb::new(
let store = HyperlaneDbStore::new(
db.clone(),
chain_setup.addresses.mailbox,
domain.clone(),
@ -74,7 +75,7 @@ impl BaseAgent for Scraper {
domain.id(),
ChainScraper {
domain: domain.clone(),
db,
store,
index_settings: chain_setup.index.clone(),
},
);
@ -132,7 +133,7 @@ impl Scraper {
/// This will spawn long-running contract sync tasks
async fn scrape(&self, domain_id: u32) -> Instrumented<JoinHandle<()>> {
let scraper = self.scrapers.get(&domain_id).unwrap();
let db = scraper.db.clone();
let store = scraper.store.clone();
let index_settings = scraper.index_settings.clone();
let domain = scraper.domain.clone();
@ -142,7 +143,7 @@ impl Scraper {
domain.clone(),
self.core_metrics.clone(),
self.contract_sync_metrics.clone(),
db.clone(),
store.clone(),
index_settings.clone(),
)
.await;
@ -152,7 +153,7 @@ impl Scraper {
domain.clone(),
self.core_metrics.clone(),
self.contract_sync_metrics.clone(),
db.clone(),
store.clone(),
index_settings.clone(),
)
.await,
@ -162,7 +163,7 @@ impl Scraper {
domain,
self.core_metrics.clone(),
self.contract_sync_metrics.clone(),
db,
store,
index_settings.clone(),
BroadcastMpscSender::<H512>::map_get_receiver(maybe_broadcaster.as_ref()).await,
)
@ -183,7 +184,7 @@ impl Scraper {
domain: HyperlaneDomain,
metrics: Arc<CoreMetrics>,
contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb,
store: HyperlaneDbStore,
index_settings: IndexSettings,
) -> (
Instrumented<JoinHandle<()>>,
@ -196,7 +197,7 @@ impl Scraper {
&domain,
&metrics.clone(),
&contract_sync_metrics.clone(),
db.into(),
store.into(),
)
.await
.unwrap();
@ -217,7 +218,7 @@ impl Scraper {
domain: HyperlaneDomain,
metrics: Arc<CoreMetrics>,
contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb,
store: HyperlaneDbStore,
index_settings: IndexSettings,
) -> Instrumented<JoinHandle<()>> {
let sync = self
@ -227,7 +228,7 @@ impl Scraper {
&domain,
&metrics.clone(),
&contract_sync_metrics.clone(),
Arc::new(db.clone()) as _,
Arc::new(store.clone()) as _,
)
.await
.unwrap();
@ -248,7 +249,7 @@ impl Scraper {
domain: HyperlaneDomain,
metrics: Arc<CoreMetrics>,
contract_sync_metrics: Arc<ContractSyncMetrics>,
db: HyperlaneSqlDb,
store: HyperlaneDbStore,
index_settings: IndexSettings,
tx_id_receiver: Option<MpscReceiver<H512>>,
) -> Instrumented<JoinHandle<()>> {
@ -259,7 +260,7 @@ impl Scraper {
&domain,
&metrics.clone(),
&contract_sync_metrics.clone(),
Arc::new(db.clone()),
Arc::new(store.clone()),
)
.await
.unwrap();

@ -31,36 +31,6 @@ pub struct StorableMessage<'a> {
}
impl ScraperDb {
/// Get the highest message nonce that is stored in the database.
#[instrument(skip(self))]
pub async fn last_message_nonce(
&self,
origin_domain: u32,
origin_mailbox: &H256,
) -> Result<Option<u32>> {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
enum QueryAs {
Nonce,
}
let last_nonce = message::Entity::find()
.filter(message::Column::Origin.eq(origin_domain))
.filter(message::Column::OriginMailbox.eq(address_to_bytes(origin_mailbox)))
.select_only()
.column_as(message::Column::Nonce.max(), QueryAs::Nonce)
.into_values::<i32, QueryAs>()
.one(&self.0)
.await?
.map(|idx| idx as u32);
debug!(
?last_nonce,
origin_domain,
?origin_mailbox,
"Queried last message nonce from database"
);
Ok(last_nonce)
}
/// Get the dispatched message associated with a nonce.
#[instrument(skip(self))]
pub async fn retrieve_message_by_nonce(

@ -17,13 +17,12 @@ use agent::Scraper;
use eyre::Result;
use hyperlane_base::agent_main;
mod db;
mod agent;
mod chain_scraper;
mod conversions;
mod date_time;
mod db;
mod settings;
mod store;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {

@ -0,0 +1,6 @@
pub use storage::HyperlaneDbStore;
mod deliveries;
mod dispatches;
mod payments;
mod storage;

@ -0,0 +1,43 @@
use std::collections::HashMap;
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{Delivery, HyperlaneLogStore, Indexed, LogMeta, H512};
use crate::db::StorableDelivery;
use crate::store::storage::{HyperlaneDbStore, TxnWithId};
#[async_trait]
impl HyperlaneLogStore<Delivery> for HyperlaneDbStore {
/// Store delivered message ids from the destination mailbox into the database.
/// We store only delivered messages ids from blocks and transaction which we could successfully
/// insert into database.
async fn store_logs(&self, deliveries: &[(Indexed<Delivery>, LogMeta)]) -> Result<u32> {
if deliveries.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, TxnWithId> = self
.ensure_blocks_and_txns(deliveries.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = deliveries
.iter()
.filter_map(|(message_id, meta)| {
txns.get(&meta.transaction_id)
.map(|txn| (*message_id.inner(), meta, txn.id))
})
.map(|(message_id, meta, txn_id)| StorableDelivery {
message_id,
meta,
txn_id,
});
let stored = self
.db
.store_deliveries(self.domain.id(), self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}

@ -0,0 +1,64 @@
use std::collections::HashMap;
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{
unwrap_or_none_result, HyperlaneLogStore, HyperlaneMessage,
HyperlaneSequenceAwareIndexerStoreReader, Indexed, LogMeta, H512,
};
use crate::db::StorableMessage;
use crate::store::storage::{HyperlaneDbStore, TxnWithId};
#[async_trait]
impl HyperlaneLogStore<HyperlaneMessage> for HyperlaneDbStore {
/// Store dispatched messages from the origin mailbox into the database.
/// We store only messages from blocks and transaction which we could successfully insert
/// into database.
async fn store_logs(&self, messages: &[(Indexed<HyperlaneMessage>, LogMeta)]) -> Result<u32> {
if messages.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, TxnWithId> = self
.ensure_blocks_and_txns(messages.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = messages
.iter()
.filter_map(|(message, meta)| {
txns.get(&meta.transaction_id)
.map(|t| (message.inner().clone(), meta, t.id))
})
.map(|(msg, meta, txn_id)| StorableMessage { msg, meta, txn_id });
let stored = self
.db
.store_dispatched_messages(self.domain.id(), &self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}
#[async_trait]
impl HyperlaneSequenceAwareIndexerStoreReader<HyperlaneMessage> for HyperlaneDbStore {
/// Gets a message by its nonce.
async fn retrieve_by_sequence(&self, sequence: u32) -> Result<Option<HyperlaneMessage>> {
let message = self
.db
.retrieve_message_by_nonce(self.domain.id(), &self.mailbox_address, sequence)
.await?;
Ok(message)
}
/// Gets the block number at which the log occurred.
async fn retrieve_log_block_number_by_sequence(&self, sequence: u32) -> Result<Option<u64>> {
let tx_id = unwrap_or_none_result!(
self.db
.retrieve_dispatched_tx_id(self.domain.id(), &self.mailbox_address, sequence)
.await?
);
let block_id = unwrap_or_none_result!(self.db.retrieve_block_id(tx_id).await?);
Ok(self.db.retrieve_block_number(block_id).await?)
}
}

@ -0,0 +1,43 @@
use std::collections::HashMap;
use async_trait::async_trait;
use eyre::Result;
use hyperlane_core::{HyperlaneLogStore, Indexed, InterchainGasPayment, LogMeta, H512};
use crate::db::StorablePayment;
use crate::store::storage::HyperlaneDbStore;
#[async_trait]
impl HyperlaneLogStore<InterchainGasPayment> for HyperlaneDbStore {
/// Store interchain gas payments into the database.
/// We store only interchain gas payments from blocks and transaction which we could
/// successfully insert into database.
async fn store_logs(
&self,
payments: &[(Indexed<InterchainGasPayment>, LogMeta)],
) -> Result<u32> {
if payments.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, crate::store::storage::TxnWithId> = self
.ensure_blocks_and_txns(payments.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = payments
.iter()
.filter_map(|(payment, meta)| {
txns.get(&meta.transaction_id)
.map(|txn| (payment.inner(), meta, txn.id))
})
.map(|(payment, meta, txn_id)| StorablePayment {
payment,
meta,
txn_id,
});
let stored = self.db.store_payments(self.domain.id(), storable).await?;
Ok(stored as u32)
}
}

@ -12,15 +12,11 @@ use tracing::{trace, warn};
use hyperlane_base::settings::IndexSettings;
use hyperlane_core::{
unwrap_or_none_result, BlockId, BlockInfo, Delivery, HyperlaneDomain, HyperlaneLogStore,
HyperlaneMessage, HyperlaneProvider, HyperlaneSequenceAwareIndexerStoreReader,
HyperlaneWatermarkedLogStore, Indexed, InterchainGasPayment, LogMeta, H256, H512,
BlockId, BlockInfo, HyperlaneDomain, HyperlaneLogStore, HyperlaneProvider,
HyperlaneWatermarkedLogStore, LogMeta, H256, H512,
};
use crate::db::{
BasicBlock, BlockCursor, ScraperDb, StorableDelivery, StorableMessage, StorablePayment,
StorableTxn,
};
use crate::db::{BasicBlock, BlockCursor, ScraperDb, StorableTxn};
/// Maximum number of records to query at a time. This came about because when a
/// lot of messages are sent in a short period of time we were ending up with a
@ -31,16 +27,16 @@ const CHUNK_SIZE: usize = 50;
/// A chain scraper is comprised of all the information and contract/provider
/// connections needed to scrape the contracts on a single blockchain.
#[derive(Clone, Debug)]
pub struct HyperlaneSqlDb {
mailbox_address: H256,
domain: HyperlaneDomain,
db: ScraperDb,
pub struct HyperlaneDbStore {
pub(crate) mailbox_address: H256,
pub(crate) domain: HyperlaneDomain,
pub(crate) db: ScraperDb,
provider: Arc<dyn HyperlaneProvider>,
cursor: Arc<BlockCursor>,
}
#[allow(unused)]
impl HyperlaneSqlDb {
impl HyperlaneDbStore {
pub async fn new(
db: ScraperDb,
mailbox_address: H256,
@ -61,21 +57,11 @@ impl HyperlaneSqlDb {
})
}
pub fn domain(&self) -> &HyperlaneDomain {
&self.domain
}
pub async fn last_message_nonce(&self) -> Result<Option<u32>> {
self.db
.last_message_nonce(self.domain.id(), &self.mailbox_address)
.await
}
/// Takes a list of txn and block hashes and ensure they are all in the
/// database. If any are not it will fetch the data and insert them.
///
/// Returns the relevant transaction info.
async fn ensure_blocks_and_txns(
pub(crate) async fn ensure_blocks_and_txns(
&self,
log_meta: impl Iterator<Item = &LogMeta>,
) -> Result<impl Iterator<Item = TxnWithId>> {
@ -262,7 +248,7 @@ impl HyperlaneSqlDb {
self.db
.store_blocks(
self.domain().id(),
self.domain.id(),
blocks_to_insert
.iter_mut()
.map(|(_, info)| info.take().unwrap()),
@ -293,129 +279,9 @@ impl HyperlaneSqlDb {
}
#[async_trait]
impl HyperlaneLogStore<HyperlaneMessage> for HyperlaneSqlDb {
/// Store dispatched messages from the origin mailbox into the database.
/// We store only messages from blocks and transaction which we could successfully insert
/// into database.
async fn store_logs(&self, messages: &[(Indexed<HyperlaneMessage>, LogMeta)]) -> Result<u32> {
if messages.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, TxnWithId> = self
.ensure_blocks_and_txns(messages.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = messages
.iter()
.filter_map(|(message, meta)| {
txns.get(&meta.transaction_id)
.map(|t| (message.inner().clone(), meta, t.id))
})
.map(|(msg, meta, txn_id)| StorableMessage { msg, meta, txn_id });
let stored = self
.db
.store_dispatched_messages(self.domain().id(), &self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}
#[async_trait]
impl HyperlaneLogStore<Delivery> for HyperlaneSqlDb {
/// Store delivered message ids from the destination mailbox into the database.
/// We store only delivered messages ids from blocks and transaction which we could successfully
/// insert into database.
async fn store_logs(&self, deliveries: &[(Indexed<Delivery>, LogMeta)]) -> Result<u32> {
if deliveries.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, TxnWithId> = self
.ensure_blocks_and_txns(deliveries.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = deliveries
.iter()
.filter_map(|(message_id, meta)| {
txns.get(&meta.transaction_id)
.map(|txn| (*message_id.inner(), meta, txn.id))
})
.map(|(message_id, meta, txn_id)| StorableDelivery {
message_id,
meta,
txn_id,
});
let stored = self
.db
.store_deliveries(self.domain().id(), self.mailbox_address, storable)
.await?;
Ok(stored as u32)
}
}
#[async_trait]
impl HyperlaneLogStore<InterchainGasPayment> for HyperlaneSqlDb {
/// Store interchain gas payments into the database.
/// We store only interchain gas payments from blocks and transaction which we could
/// successfully insert into database.
async fn store_logs(
&self,
payments: &[(Indexed<InterchainGasPayment>, LogMeta)],
) -> Result<u32> {
if payments.is_empty() {
return Ok(0);
}
let txns: HashMap<H512, TxnWithId> = self
.ensure_blocks_and_txns(payments.iter().map(|r| &r.1))
.await?
.map(|t| (t.hash, t))
.collect();
let storable = payments
.iter()
.filter_map(|(payment, meta)| {
txns.get(&meta.transaction_id)
.map(|txn| (payment.inner(), meta, txn.id))
})
.map(|(payment, meta, txn_id)| StorablePayment {
payment,
meta,
txn_id,
});
let stored = self.db.store_payments(self.domain().id(), storable).await?;
Ok(stored as u32)
}
}
#[async_trait]
impl HyperlaneSequenceAwareIndexerStoreReader<HyperlaneMessage> for HyperlaneSqlDb {
/// Gets a message by its nonce.
async fn retrieve_by_sequence(&self, sequence: u32) -> Result<Option<HyperlaneMessage>> {
let message = self
.db
.retrieve_message_by_nonce(self.domain().id(), &self.mailbox_address, sequence)
.await?;
Ok(message)
}
/// Gets the block number at which the log occurred.
async fn retrieve_log_block_number_by_sequence(&self, sequence: u32) -> Result<Option<u64>> {
let tx_id = unwrap_or_none_result!(
self.db
.retrieve_dispatched_tx_id(self.domain().id(), &self.mailbox_address, sequence)
.await?
);
let block_id = unwrap_or_none_result!(self.db.retrieve_block_id(tx_id).await?);
Ok(self.db.retrieve_block_number(block_id).await?)
}
}
#[async_trait]
impl<T> HyperlaneWatermarkedLogStore<T> for HyperlaneSqlDb
impl<T> HyperlaneWatermarkedLogStore<T> for HyperlaneDbStore
where
HyperlaneSqlDb: HyperlaneLogStore<T>,
HyperlaneDbStore: HyperlaneLogStore<T>,
{
/// Gets the block number high watermark
async fn retrieve_high_watermark(&self) -> Result<Option<u32>> {
@ -429,9 +295,9 @@ where
}
#[derive(Debug, Clone)]
struct TxnWithId {
hash: H512,
id: i64,
pub(crate) struct TxnWithId {
pub hash: H512,
pub id: i64,
}
#[derive(Debug, Clone)]

@ -334,6 +334,7 @@ where
tx,
self.provider.clone(),
&self.conn.transaction_overrides.clone(),
&self.domain,
)
.await
}
@ -382,6 +383,7 @@ where
call,
provider: self.provider.clone(),
transaction_overrides: self.conn.transaction_overrides.clone(),
domain: self.domain.clone(),
}
}
}
@ -418,12 +420,18 @@ pub struct SubmittableBatch<M> {
pub call: ContractCall<M, Vec<MulticallResult>>,
provider: Arc<M>,
transaction_overrides: TransactionOverrides,
domain: HyperlaneDomain,
}
impl<M: Middleware + 'static> SubmittableBatch<M> {
pub async fn submit(self) -> ChainResult<TxOutcome> {
let call_with_gas_overrides =
fill_tx_gas_params(self.call, self.provider, &self.transaction_overrides).await?;
let call_with_gas_overrides = fill_tx_gas_params(
self.call,
self.provider,
&self.transaction_overrides,
&self.domain,
)
.await?;
let outcome = report_tx(call_with_gas_overrides).await?;
Ok(outcome.into())
}
@ -615,10 +623,10 @@ mod test {
TxCostEstimate, H160, H256, U256,
};
use crate::{contracts::EthereumMailbox, ConnectionConf, RpcConnectionConf};
/// An amount of gas to add to the estimated gas
const GAS_ESTIMATE_BUFFER: u32 = 75_000;
use crate::{
contracts::EthereumMailbox, tx::apply_gas_estimate_buffer, ConnectionConf,
RpcConnectionConf,
};
fn get_test_mailbox(
domain: HyperlaneDomain,
@ -650,9 +658,9 @@ mod test {
#[tokio::test]
async fn test_process_estimate_costs_sets_l2_gas_limit_for_arbitrum() {
let domain = HyperlaneDomain::Known(KnownHyperlaneDomain::PlumeTestnet);
// An Arbitrum Nitro chain
let (mailbox, mock_provider) =
get_test_mailbox(HyperlaneDomain::Known(KnownHyperlaneDomain::PlumeTestnet));
let (mailbox, mock_provider) = get_test_mailbox(domain.clone());
let message = HyperlaneMessage::default();
let metadata: Vec<u8> = vec![];
@ -696,8 +704,8 @@ mod test {
.await
.unwrap();
// The TxCostEstimate's gas limit includes the buffer
let estimated_gas_limit = gas_limit.saturating_add(GAS_ESTIMATE_BUFFER.into());
// The TxCostEstimate's gas limit includes a buffer
let estimated_gas_limit = apply_gas_estimate_buffer(gas_limit, &domain).unwrap();
assert_eq!(
tx_cost_estimate,

@ -92,7 +92,13 @@ where
announcement.value.storage_location,
serialized_signature.into(),
);
fill_tx_gas_params(tx, self.provider.clone(), &self.conn.transaction_overrides).await
fill_tx_gas_params(
tx,
self.provider.clone(),
&self.conn.transaction_overrides,
&self.domain,
)
.await
}
}

@ -16,7 +16,8 @@ use ethers_core::{
},
};
use hyperlane_core::{
utils::bytes_to_hex, ChainCommunicationError, ChainResult, ReorgPeriod, H256, U256,
utils::bytes_to_hex, ChainCommunicationError, ChainResult, HyperlaneDomain, ReorgPeriod, H256,
U256,
};
use tracing::{debug, error, info, warn};
@ -25,8 +26,26 @@ use crate::{EthereumReorgPeriod, Middleware, TransactionOverrides};
/// An amount of gas to add to the estimated gas
pub const GAS_ESTIMATE_BUFFER: u32 = 75_000;
pub fn apply_gas_estimate_buffer(gas: U256) -> U256 {
gas.saturating_add(GAS_ESTIMATE_BUFFER.into())
// A multiplier to apply to the estimated gas, i.e. 10%.
pub const GAS_ESTIMATE_MULTIPLIER_NUMERATOR: u32 = 11;
pub const GAS_ESTIMATE_MULTIPLIER_DENOMINATOR: u32 = 10;
pub fn apply_gas_estimate_buffer(gas: U256, domain: &HyperlaneDomain) -> ChainResult<U256> {
// Arbitrum Nitro chains use 2d fees are are especially prone to costs increasing
// by the time the transaction lands on chain, requiring a higher gas limit.
// In this case, we apply a multiplier to the gas estimate.
let gas = if domain.is_arbitrum_nitro() {
gas.saturating_mul(GAS_ESTIMATE_MULTIPLIER_NUMERATOR.into())
.checked_div(GAS_ESTIMATE_MULTIPLIER_DENOMINATOR.into())
.ok_or_else(|| {
ChainCommunicationError::from_other_str("Gas estimate buffer divide by zero")
})?
} else {
gas
};
// Always add a flat buffer
Ok(gas.saturating_add(GAS_ESTIMATE_BUFFER.into()))
}
const PENDING_TRANSACTION_POLLING_INTERVAL: Duration = Duration::from_secs(2);
@ -92,17 +111,19 @@ pub(crate) async fn fill_tx_gas_params<M, D>(
tx: ContractCall<M, D>,
provider: Arc<M>,
transaction_overrides: &TransactionOverrides,
domain: &HyperlaneDomain,
) -> ChainResult<ContractCall<M, D>>
where
M: Middleware + 'static,
D: Detokenize,
{
// either use the pre-estimated gas limit or estimate it
let estimated_gas_limit: U256 = match tx.tx.gas() {
let mut estimated_gas_limit: U256 = match tx.tx.gas() {
Some(&estimate) => estimate.into(),
None => tx.estimate_gas().await?.into(),
};
let estimated_gas_limit = apply_gas_estimate_buffer(estimated_gas_limit);
estimated_gas_limit = apply_gas_estimate_buffer(estimated_gas_limit, domain)?;
let gas_limit: U256 = if let Some(gas_limit) = transaction_overrides.gas_limit {
estimated_gas_limit.max(gas_limit)
} else {

@ -12,7 +12,7 @@ use tracing::{info, instrument};
use hyperlane_core::{
config::StrOrIntParseError, ChainCommunicationError, ChainResult, ContractLocator,
HyperlaneChain, HyperlaneContract, HyperlaneDomain, HyperlaneProvider, Indexed, Indexer,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, U256,
InterchainGasPaymaster, InterchainGasPayment, LogMeta, SequenceAwareIndexer, H256, H512, U256,
};
use crate::account::{search_accounts_by_discriminator, search_and_validate_account};
@ -91,7 +91,7 @@ impl InterchainGasPaymaster for SealevelInterchainGasPaymaster {}
pub struct SealevelInterchainGasPaymasterIndexer {
rpc_client: SealevelRpcClient,
igp: SealevelInterchainGasPaymaster,
log_meta_composer: LogMetaComposer,
_log_meta_composer: LogMetaComposer,
}
/// IGP payment data on Sealevel
@ -122,7 +122,7 @@ impl SealevelInterchainGasPaymasterIndexer {
Ok(Self {
rpc_client,
igp,
log_meta_composer,
_log_meta_composer: log_meta_composer,
})
}
@ -168,13 +168,24 @@ impl SealevelInterchainGasPaymasterIndexer {
gas_amount: gas_payment_account.gas_amount.into(),
};
let log_meta = self
.interchain_payment_log_meta(
U256::from(sequence_number),
&valid_payment_pda_pubkey,
&gas_payment_account.slot,
)
.await?;
// let log_meta = self
// .interchain_payment_log_meta(
// U256::from(sequence_number),
// &valid_payment_pda_pubkey,
// &gas_payment_account.slot,
// )
// .await?;
let log_meta = LogMeta {
address: self.igp.program_id.to_bytes().into(),
block_number: gas_payment_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: sequence_number.into(),
};
Ok(SealevelGasPayment::new(
Indexed::new(igp_payment).with_sequence(
@ -187,19 +198,6 @@ impl SealevelInterchainGasPaymasterIndexer {
))
}
async fn interchain_payment_log_meta(
&self,
log_index: U256,
payment_pda_pubkey: &Pubkey,
payment_pda_slot: &Slot,
) -> ChainResult<LogMeta> {
let block = self.rpc_client.get_block(*payment_pda_slot).await?;
self.log_meta_composer
.log_meta(block, log_index, payment_pda_pubkey, payment_pda_slot)
.map_err(Into::<ChainCommunicationError>::into)
}
fn interchain_payment_account(&self, account: &Account) -> ChainResult<Pubkey> {
let unique_gas_payment_pubkey = Pubkey::new(&account.data);
let (expected_pubkey, _bump) = Pubkey::try_find_program_address(
@ -213,6 +211,19 @@ impl SealevelInterchainGasPaymasterIndexer {
})?;
Ok(expected_pubkey)
}
async fn _interchain_payment_log_meta(
&self,
log_index: U256,
payment_pda_pubkey: &Pubkey,
payment_pda_slot: &Slot,
) -> ChainResult<LogMeta> {
let block = self.rpc_client.get_block(*payment_pda_slot).await?;
self._log_meta_composer
.log_meta(block, log_index, payment_pda_pubkey, payment_pda_slot)
.map_err(Into::<ChainCommunicationError>::into)
}
}
#[async_trait]
@ -246,7 +257,10 @@ impl Indexer<InterchainGasPayment> for SealevelInterchainGasPaymasterIndexer {
#[instrument(level = "debug", err, ret, skip(self))]
#[allow(clippy::blocks_in_conditions)] // TODO: `rustc` 1.80.1 clippy issue
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.rpc_client.get_block_height().await
// we should not report block height since SequenceAwareIndexer uses block slot in
// `latest_sequence_count_and_tip` and we should not report block slot here
// since block slot cannot be used as watermark
unimplemented!()
}
}
@ -266,7 +280,7 @@ impl SequenceAwareIndexer<InterchainGasPayment> for SealevelInterchainGasPaymast
.payment_count
.try_into()
.map_err(StrOrIntParseError::from)?;
let tip = self.rpc_client.get_block_height().await?;
let tip = self.igp.provider.rpc().get_slot().await?;
Ok((Some(payment_count), tip))
}
}

@ -679,10 +679,6 @@ impl SealevelMailboxIndexer {
&self.mailbox.rpc()
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.rpc().get_block_height().await
}
async fn get_dispatched_message_with_nonce(
&self,
nonce: u32,
@ -716,13 +712,24 @@ impl SealevelMailboxIndexer {
let hyperlane_message =
HyperlaneMessage::read_from(&mut &dispatched_message_account.encoded_message[..])?;
let log_meta = self
.dispatch_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&dispatched_message_account.slot,
)
.await?;
// let log_meta = self
// .dispatch_message_log_meta(
// U256::from(nonce),
// &valid_message_storage_pda_pubkey,
// &dispatched_message_account.slot,
// )
// .await?;
let log_meta = LogMeta {
address: self.program_id.to_bytes().into(),
block_number: dispatched_message_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: U256::zero(),
};
Ok((hyperlane_message.into(), log_meta))
}
@ -741,7 +748,7 @@ impl SealevelMailboxIndexer {
Ok(expected_pubkey)
}
async fn dispatch_message_log_meta(
async fn _dispatch_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
@ -798,13 +805,24 @@ impl SealevelMailboxIndexer {
.into_inner();
let message_id = delivered_message_account.message_id;
let log_meta = self
.delivered_message_log_meta(
U256::from(nonce),
&valid_message_storage_pda_pubkey,
&delivered_message_account.slot,
)
.await?;
// let log_meta = self
// .delivered_message_log_meta(
// U256::from(nonce),
// &valid_message_storage_pda_pubkey,
// &delivered_message_account.slot,
// )
// .await?;
let log_meta = LogMeta {
address: self.program_id.to_bytes().into(),
block_number: delivered_message_account.slot,
// TODO: get these when building out scraper support.
// It's inconvenient to get these :|
block_hash: H256::zero(),
transaction_id: H512::zero(),
transaction_index: 0,
log_index: U256::zero(),
};
Ok((message_id.into(), log_meta))
}
@ -821,7 +839,7 @@ impl SealevelMailboxIndexer {
Ok(expected_pubkey)
}
async fn delivered_message_log_meta(
async fn _delivered_message_log_meta(
&self,
log_index: U256,
message_storage_pda_pubkey: &Pubkey,
@ -845,17 +863,6 @@ impl SealevelMailboxIndexer {
}
}
#[async_trait]
impl SequenceAwareIndexer<HyperlaneMessage> for SealevelMailboxIndexer {
#[instrument(err, skip(self))]
async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
let tip = Indexer::<HyperlaneMessage>::get_finalized_block_number(self).await?;
// TODO: need to make sure the call and tip are at the same height?
let count = Mailbox::count(&self.mailbox, &ReorgPeriod::None).await?;
Ok((Some(count), tip))
}
}
#[async_trait]
impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
async fn fetch_logs_in_range(
@ -876,7 +883,21 @@ impl Indexer<HyperlaneMessage> for SealevelMailboxIndexer {
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.get_finalized_block_number().await
// we should not report block height since SequenceAwareIndexer uses block slot in
// `latest_sequence_count_and_tip` and we should not report block slot here
// since block slot cannot be used as watermark
unimplemented!()
}
}
#[async_trait]
impl SequenceAwareIndexer<HyperlaneMessage> for SealevelMailboxIndexer {
#[instrument(err, skip(self))]
async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
let tip = self.mailbox.provider.rpc().get_slot().await?;
// TODO: need to make sure the call and tip are at the same height?
let count = Mailbox::count(&self.mailbox, &ReorgPeriod::None).await?;
Ok((Some(count), tip))
}
}
@ -900,7 +921,10 @@ impl Indexer<H256> for SealevelMailboxIndexer {
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
self.get_finalized_block_number().await
// we should not report block height since SequenceAwareIndexer uses block slot in
// `latest_sequence_count_and_tip` and we should not report block slot here
// since block slot cannot be used as watermark
unimplemented!()
}
}
@ -909,7 +933,7 @@ impl SequenceAwareIndexer<H256> for SealevelMailboxIndexer {
async fn latest_sequence_count_and_tip(&self) -> ChainResult<(Option<u32>, u32)> {
// TODO: implement when sealevel scraper support is implemented
info!("Message delivery indexing not implemented");
let tip = Indexer::<H256>::get_finalized_block_number(self).await?;
let tip = self.mailbox.provider.rpc().get_slot().await?;
Ok((Some(1), tip))
}
}

@ -93,7 +93,10 @@ impl Indexer<MerkleTreeInsertion> for SealevelMerkleTreeHookIndexer {
}
async fn get_finalized_block_number(&self) -> ChainResult<u32> {
Indexer::<HyperlaneMessage>::get_finalized_block_number(&self.0).await
// we should not report block height since SequenceAwareIndexer uses block slot in
// `latest_sequence_count_and_tip` and we should not report block slot here
// since block slot cannot be used as watermark
unimplemented!()
}
}

@ -125,18 +125,6 @@ impl SealevelRpcClient {
.map_err(Into::into)
}
pub async fn get_block_height(&self) -> ChainResult<u32> {
let height = self
.0
.get_block_height_with_commitment(CommitmentConfig::finalized())
.await
.map_err(ChainCommunicationError::from_other)?
.try_into()
// FIXME solana block height is u64...
.expect("sealevel block height exceeds u32::MAX");
Ok(height)
}
pub async fn get_multiple_accounts_with_finalized_commitment(
&self,
pubkeys: &[Pubkey],
@ -183,6 +171,18 @@ impl SealevelRpcClient {
.map_err(ChainCommunicationError::from_other)
}
pub async fn get_slot(&self) -> ChainResult<u32> {
let slot = self
.0
.get_slot_with_commitment(CommitmentConfig::finalized())
.await
.map_err(ChainCommunicationError::from_other)?
.try_into()
// FIXME solana block height is u64...
.expect("sealevel block slot exceeds u32::MAX");
Ok(slot)
}
pub async fn get_transaction(
&self,
signature: &Signature,

@ -1231,7 +1231,7 @@
"bech32Prefix": "inj",
"blockExplorers": [
{
"apiUrl": "https://www.mintscan.io/injective",
"apiUrl": "https://apis.mintscan.io/v1/injective",
"family": "other",
"name": "Mintscan",
"url": "https://www.mintscan.io/injective"
@ -2042,7 +2042,7 @@
"bech32Prefix": "neutron",
"blockExplorers": [
{
"apiUrl": "https://www.mintscan.io/neutron",
"apiUrl": "https://apis.mintscan.io/v1/neutron",
"family": "other",
"name": "Mintscan",
"url": "https://www.mintscan.io/neutron"
@ -2180,7 +2180,7 @@
"bech32Prefix": "osmo",
"blockExplorers": [
{
"apiUrl": "https://www.mintscan.io/osmosis",
"apiUrl": "https://apis.mintscan.io/v1/osmosis",
"family": "other",
"name": "Mintscan",
"url": "https://www.mintscan.io/osmosis"
@ -4111,7 +4111,7 @@
"bech32Prefix": "stride",
"blockExplorers": [
{
"apiUrl": "https://www.mintscan.io/stride",
"apiUrl": "https://apis.mintscan.io/v1/stride",
"family": "other",
"name": "Mintscan",
"url": "https://www.mintscan.io/stride"

@ -17,6 +17,7 @@ spec:
metadata:
annotations:
checksum/configmap: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/relayer-configmap: {{ include (print $.Template.BasePath "/relayer-configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}

@ -78,7 +78,7 @@ pub enum SyncDirection {
/// performed by `next_action`.
pub(crate) struct RateLimitedContractSyncCursor<T> {
indexer: Arc<dyn Indexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
store: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
tip: u32,
last_tip_update: Instant,
eta_calculator: SyncerEtaCalculator,
@ -89,14 +89,14 @@ impl<T> RateLimitedContractSyncCursor<T> {
/// Construct a new contract sync helper.
pub async fn new(
indexer: Arc<dyn Indexer<T>>,
db: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
store: Arc<dyn HyperlaneWatermarkedLogStore<T>>,
chunk_size: u32,
initial_height: u32,
) -> Result<Self> {
let tip = indexer.get_finalized_block_number().await?;
Ok(Self {
indexer,
db,
store,
tip,
last_tip_update: Instant::now(),
eta_calculator: SyncerEtaCalculator::new(initial_height, tip, ETA_TIME_WINDOW),
@ -189,7 +189,7 @@ where
) -> Result<()> {
// Store a relatively conservative view of the high watermark, which should allow a single watermark to be
// safely shared across multiple cursors, so long as they are running sufficiently in sync
self.db
self.store
.store_high_watermark(u32::max(
self.sync_state.start_block,
self.sync_state

@ -22,8 +22,8 @@ pub(crate) struct BackwardSequenceAwareSyncCursor<T> {
/// If in sequence mode, this is the max number of sequences to query.
/// If in block mode, this is the max number of blocks to query.
chunk_size: u32,
/// A DB used to check which logs have already been indexed.
db: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
/// A store used to check which logs have already been indexed.
store: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
/// A snapshot of the last log to be indexed, or if no indexing has occurred yet,
/// the initial log to start indexing backward from.
last_indexed_snapshot: LastIndexedSnapshot,
@ -48,13 +48,13 @@ impl<T> Debug for BackwardSequenceAwareSyncCursor<T> {
impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
#[instrument(
skip(db),
skip(store),
fields(chunk_size, next_sequence, start_block, index_mode),
ret
)]
pub fn new(
chunk_size: u32,
db: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
store: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
current_sequence_count: u32,
start_block: u32,
index_mode: IndexMode,
@ -69,7 +69,7 @@ impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
Self {
chunk_size,
db,
store,
current_indexing_snapshot: last_indexed_snapshot.previous_target(),
last_indexed_snapshot,
index_mode,
@ -166,10 +166,10 @@ impl<T: Debug> BackwardSequenceAwareSyncCursor<T> {
/// log for the sequence number hasn't been indexed.
async fn get_sequence_log_block_number(&self, sequence: u32) -> Result<Option<u32>> {
// Ensure there's a full entry for the sequence.
if self.db.retrieve_by_sequence(sequence).await?.is_some() {
if self.store.retrieve_by_sequence(sequence).await?.is_some() {
// And get the block number.
if let Some(block_number) = self
.db
.store
.retrieve_log_block_number_by_sequence(sequence)
.await?
{

@ -27,8 +27,8 @@ pub(crate) struct ForwardSequenceAwareSyncCursor<T> {
/// This is used to check if there are new logs to index and to
/// establish targets to index towards.
latest_sequence_querier: Arc<dyn SequenceAwareIndexer<T>>,
/// A DB used to check which logs have already been indexed.
db: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
/// A store used to check which logs have already been indexed.
store: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
/// A snapshot of the last indexed log, or if no indexing has occurred yet,
/// the initial log to start indexing forward from.
last_indexed_snapshot: LastIndexedSnapshot,
@ -55,14 +55,14 @@ impl<T> Debug for ForwardSequenceAwareSyncCursor<T> {
impl<T: Debug> ForwardSequenceAwareSyncCursor<T> {
#[instrument(
skip(db, latest_sequence_querier),
skip(store, latest_sequence_querier),
fields(chunk_size, next_sequence, start_block, index_mode),
ret
)]
pub fn new(
chunk_size: u32,
latest_sequence_querier: Arc<dyn SequenceAwareIndexer<T>>,
db: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
store: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
next_sequence: u32,
start_block: u32,
index_mode: IndexMode,
@ -77,7 +77,7 @@ impl<T: Debug> ForwardSequenceAwareSyncCursor<T> {
Self {
chunk_size,
latest_sequence_querier,
db,
store,
last_indexed_snapshot,
current_indexing_snapshot: TargetSnapshot {
sequence: next_sequence,
@ -221,10 +221,10 @@ impl<T: Debug> ForwardSequenceAwareSyncCursor<T> {
/// log for the sequence number hasn't been indexed.
async fn get_sequence_log_block_number(&self, sequence: u32) -> Result<Option<u32>> {
// Ensure there's a full entry for the sequence.
if self.db.retrieve_by_sequence(sequence).await?.is_some() {
if self.store.retrieve_by_sequence(sequence).await?.is_some() {
// And get the block number.
if let Some(block_number) = self
.db
.store
.retrieve_log_block_number_by_sequence(sequence)
.await?
{

@ -73,7 +73,7 @@ impl<T: Debug> ForwardBackwardSequenceAwareSyncCursor<T> {
/// Construct a new contract sync helper.
pub async fn new(
latest_sequence_querier: Arc<dyn SequenceAwareIndexer<T>>,
db: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
store: Arc<dyn HyperlaneSequenceAwareIndexerStoreReader<T>>,
chunk_size: u32,
mode: IndexMode,
) -> Result<Self> {
@ -86,13 +86,13 @@ impl<T: Debug> ForwardBackwardSequenceAwareSyncCursor<T> {
let forward_cursor = ForwardSequenceAwareSyncCursor::new(
chunk_size,
latest_sequence_querier.clone(),
db.clone(),
store.clone(),
sequence_count,
tip,
mode,
);
let backward_cursor =
BackwardSequenceAwareSyncCursor::new(chunk_size, db, sequence_count, tip, mode);
BackwardSequenceAwareSyncCursor::new(chunk_size, store, sequence_count, tip, mode);
Ok(Self {
forward: forward_cursor,
backward: backward_cursor,

@ -43,21 +43,26 @@ struct IndexedTxIdAndSequence {
/// Extracts chain-specific data (emitted checkpoints, messages, etc) from an
/// `indexer` and fills the agent's db with this data.
#[derive(Debug)]
pub struct ContractSync<T: Indexable, D: HyperlaneLogStore<T>, I: Indexer<T>> {
pub struct ContractSync<T: Indexable, S: HyperlaneLogStore<T>, I: Indexer<T>> {
domain: HyperlaneDomain,
db: D,
store: S,
indexer: I,
metrics: ContractSyncMetrics,
broadcast_sender: Option<BroadcastMpscSender<H512>>,
_phantom: PhantomData<T>,
}
impl<T: Indexable, D: HyperlaneLogStore<T>, I: Indexer<T>> ContractSync<T, D, I> {
impl<T: Indexable, S: HyperlaneLogStore<T>, I: Indexer<T>> ContractSync<T, S, I> {
/// Create a new ContractSync
pub fn new(domain: HyperlaneDomain, db: D, indexer: I, metrics: ContractSyncMetrics) -> Self {
pub fn new(
domain: HyperlaneDomain,
store: S,
indexer: I,
metrics: ContractSyncMetrics,
) -> Self {
Self {
domain,
db,
store,
indexer,
metrics,
broadcast_sender: T::broadcast_channel_size().map(BroadcastMpscSender::new),
@ -66,10 +71,10 @@ impl<T: Indexable, D: HyperlaneLogStore<T>, I: Indexer<T>> ContractSync<T, D, I>
}
}
impl<T, D, I> ContractSync<T, D, I>
impl<T, S, I> ContractSync<T, S, I>
where
T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static,
D: HyperlaneLogStore<T>,
S: HyperlaneLogStore<T>,
I: Indexer<T> + 'static,
{
/// The domain that this ContractSync is running on
@ -221,7 +226,7 @@ where
let logs = Vec::from_iter(deduped_logs);
// Store deliveries
let stored = match self.db.store_logs(&logs).await {
let stored = match self.store.store_logs(&logs).await {
Ok(stored) => stored,
Err(err) => {
warn!(?err, "Error storing logs in db");
@ -298,7 +303,7 @@ where
&self,
index_settings: IndexSettings,
) -> Result<Box<dyn ContractSyncCursor<T>>> {
let watermark = self.db.retrieve_high_watermark().await.unwrap();
let watermark = self.store.retrieve_high_watermark().await.unwrap();
let index_settings = IndexSettings {
from: watermark.unwrap_or(index_settings.from),
chunk_size: index_settings.chunk_size,
@ -307,7 +312,7 @@ where
Ok(Box::new(
RateLimitedContractSyncCursor::new(
Arc::new(self.indexer.clone()),
self.db.clone(),
self.store.clone(),
index_settings.chunk_size,
index_settings.from,
)
@ -348,7 +353,7 @@ where
Ok(Box::new(
ForwardBackwardSequenceAwareSyncCursor::new(
self.indexer.clone(),
Arc::new(self.db.clone()),
Arc::new(self.store.clone()),
index_settings.chunk_size,
index_settings.mode,
)

@ -152,48 +152,48 @@ impl Settings {
build_contract_fns!(build_provider, build_providers -> dyn HyperlaneProvider);
/// Build a contract sync for type `T` using log store `D`
pub async fn sequenced_contract_sync<T, D>(
pub async fn sequenced_contract_sync<T, S>(
&self,
domain: &HyperlaneDomain,
metrics: &CoreMetrics,
sync_metrics: &ContractSyncMetrics,
db: Arc<D>,
store: Arc<S>,
) -> eyre::Result<Arc<SequencedDataContractSync<T>>>
where
T: Indexable + Debug,
SequenceIndexer<T>: TryFromWithMetrics<ChainConf>,
D: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + 'static,
S: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + 'static,
{
let setup = self.chain_setup(domain)?;
// Currently, all indexers are of the `SequenceIndexer` type
let indexer = SequenceIndexer::<T>::try_from_with_metrics(setup, metrics).await?;
Ok(Arc::new(ContractSync::new(
domain.clone(),
db.clone() as SequenceAwareLogStore<_>,
store.clone() as SequenceAwareLogStore<_>,
indexer,
sync_metrics.clone(),
)))
}
/// Build a contract sync for type `T` using log store `D`
pub async fn watermark_contract_sync<T, D>(
pub async fn watermark_contract_sync<T, S>(
&self,
domain: &HyperlaneDomain,
metrics: &CoreMetrics,
sync_metrics: &ContractSyncMetrics,
db: Arc<D>,
store: Arc<S>,
) -> eyre::Result<Arc<WatermarkContractSync<T>>>
where
T: Indexable + Debug,
SequenceIndexer<T>: TryFromWithMetrics<ChainConf>,
D: HyperlaneLogStore<T> + HyperlaneWatermarkedLogStore<T> + 'static,
S: HyperlaneLogStore<T> + HyperlaneWatermarkedLogStore<T> + 'static,
{
let setup = self.chain_setup(domain)?;
// Currently, all indexers are of the `SequenceIndexer` type
let indexer = SequenceIndexer::<T>::try_from_with_metrics(setup, metrics).await?;
Ok(Arc::new(ContractSync::new(
domain.clone(),
db.clone() as WatermarkLogStore<_>,
store.clone() as WatermarkLogStore<_>,
indexer,
sync_metrics.clone(),
)))
@ -202,17 +202,17 @@ impl Settings {
/// Build multiple contract syncs.
/// All contracts have to implement both sequenced and
/// watermark trait bounds
pub async fn contract_syncs<T, D>(
pub async fn contract_syncs<T, S>(
&self,
domains: impl Iterator<Item = &HyperlaneDomain>,
metrics: &CoreMetrics,
sync_metrics: &ContractSyncMetrics,
dbs: HashMap<HyperlaneDomain, Arc<D>>,
stores: HashMap<HyperlaneDomain, Arc<S>>,
) -> Result<HashMap<HyperlaneDomain, Arc<dyn ContractSyncer<T>>>>
where
T: Indexable + Debug + Send + Sync + Clone + Eq + Hash + 'static,
SequenceIndexer<T>: TryFromWithMetrics<ChainConf>,
D: HyperlaneLogStore<T>
S: HyperlaneLogStore<T>
+ HyperlaneSequenceAwareIndexerStoreReader<T>
+ HyperlaneWatermarkedLogStore<T>
+ 'static,
@ -226,7 +226,7 @@ impl Settings {
domain,
metrics,
sync_metrics,
dbs.get(domain).unwrap().clone(),
stores.get(domain).unwrap().clone(),
)
.await
.map(|r| r as Arc<dyn ContractSyncer<T>>)?,
@ -235,7 +235,7 @@ impl Settings {
domain,
metrics,
sync_metrics,
dbs.get(domain).unwrap().clone(),
stores.get(domain).unwrap().clone(),
)
.await
.map(|r| r as Arc<dyn ContractSyncer<T>>)?,

@ -42,8 +42,8 @@ pub trait HyperlaneSequenceAwareIndexerStore<T>:
}
/// Auto-impl for HyperlaneSequenceAwareIndexerStore
impl<T, U> HyperlaneSequenceAwareIndexerStore<T> for U where
U: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + Send + Sync + Debug
impl<T, S> HyperlaneSequenceAwareIndexerStore<T> for S where
S: HyperlaneLogStore<T> + HyperlaneSequenceAwareIndexerStoreReader<T> + Send + Sync + Debug
{
}

@ -74,12 +74,7 @@ impl Debug for HyperlaneMessage {
impl Display for HyperlaneMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"HyperlaneMessage {{ id: {:?}, nonce: {}, .. }}",
self.id(),
self.nonce
)
Debug::fmt(self, f)
}
}

@ -120,10 +120,9 @@ fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], init: Init) -> Prog
/// 12. `[]` OPTIONAL - The Overhead IGP program, if the configured IGP is an Overhead IGP.
/// 13. `[writeable]` The IGP account.
/// ---- End if ----
/// 14. `[signer]` The token sender.
/// 15. `[executable]` The spl_token_2022 program.
/// 16. `[writeable]` The mint / mint authority PDA account.
/// 17. `[writeable]` The token sender's associated token account, from which tokens will be burned.
/// 14. `[executable]` The spl_token_2022 program.
/// 15. `[writeable]` The mint / mint authority PDA account.
/// 16. `[writeable]` The token sender's associated token account, from which tokens will be burned.
fn transfer_remote(
program_id: &Pubkey,
accounts: &[AccountInfo],

@ -1,5 +1,12 @@
# @hyperlane-xyz/core
## 5.8.1
### Patch Changes
- Updated dependencies [0e285a443]
- @hyperlane-xyz/utils@7.1.0
## 5.8.0
### Minor Changes

@ -7,5 +7,5 @@ pragma solidity >=0.6.11;
**/
abstract contract PackageVersioned {
// GENERATED CODE - DO NOT EDIT
string public constant PACKAGE_VERSION = "5.8.0";
string public constant PACKAGE_VERSION = "5.8.1";
}

@ -1,11 +1,11 @@
{
"name": "@hyperlane-xyz/core",
"description": "Core solidity contracts for Hyperlane",
"version": "5.8.0",
"version": "5.8.1",
"dependencies": {
"@arbitrum/nitro-contracts": "^1.2.1",
"@eth-optimism/contracts": "^0.6.0",
"@hyperlane-xyz/utils": "7.0.0",
"@hyperlane-xyz/utils": "7.1.0",
"@layerzerolabs/lz-evm-oapp-v2": "2.0.2",
"@openzeppelin/contracts": "^4.9.3",
"@openzeppelin/contracts-upgradeable": "^v4.9.3",

@ -1,5 +1,7 @@
# @hyperlane-xyz/ccip-server
## 7.1.0
## 7.0.0
## 6.0.0

@ -1,6 +1,6 @@
{
"name": "@hyperlane-xyz/ccip-server",
"version": "7.0.0",
"version": "7.1.0",
"description": "CCIP server",
"typings": "dist/index.d.ts",
"typedocMain": "src/index.ts",

@ -1,6 +1,5 @@
{
"extensions": ["ts"],
"spec": ["src/**/*.e2e-test.ts"],
"node-option": [
"experimental-specifier-resolution=node",
"loader=ts-node/esm"

@ -1,5 +1,23 @@
# @hyperlane-xyz/cli
## 7.1.0
### Minor Changes
- 5db46bd31: Implements persistent relayer for use in CLI
### Patch Changes
- Updated dependencies [6f2d50fbd]
- Updated dependencies [1159e0f4b]
- Updated dependencies [0e285a443]
- Updated dependencies [ff2b4e2fb]
- Updated dependencies [0e285a443]
- Updated dependencies [5db46bd31]
- Updated dependencies [0cd65c571]
- @hyperlane-xyz/sdk@7.1.0
- @hyperlane-xyz/utils@7.1.0
## 7.0.0
### Minor Changes

@ -1,13 +1,13 @@
{
"name": "@hyperlane-xyz/cli",
"version": "7.0.0",
"version": "7.1.0",
"description": "A command-line utility for common Hyperlane operations",
"dependencies": {
"@aws-sdk/client-kms": "^3.577.0",
"@aws-sdk/client-s3": "^3.577.0",
"@hyperlane-xyz/registry": "4.7.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.1.0",
"@hyperlane-xyz/utils": "7.1.0",
"@inquirer/core": "9.0.10",
"@inquirer/figures": "1.0.5",
"@inquirer/prompts": "^3.0.0",

@ -16,7 +16,12 @@ anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state --gas-price 1 > /dev/nu
anvil --chain-id 31347 -p 8600 --state /tmp/anvil3/state --gas-price 1 > /dev/null &
echo "Running E2E tests"
yarn mocha --config .mocharc-e2e.json
if [ -n "${CLI_E2E_TEST}" ]; then
echo "Running only ${CLI_E2E_TEST} test"
yarn mocha --config .mocharc-e2e.json "src/**/${CLI_E2E_TEST}.e2e-test.ts"
else
yarn mocha --config .mocharc-e2e.json "src/**/*.e2e-test.ts"
fi
cleanup

@ -1 +1 @@
export const VERSION = '7.0.0';
export const VERSION = '7.1.0';

@ -1,5 +1,7 @@
# @hyperlane-xyz/github-proxy
## 7.1.0
## 7.0.0
## 6.0.0

@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/github-proxy",
"description": "Github proxy that adds the API key to requests",
"version": "7.0.0",
"version": "7.1.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",

@ -1,5 +1,18 @@
# @hyperlane-xyz/helloworld
## 7.1.0
### Patch Changes
- Updated dependencies [6f2d50fbd]
- Updated dependencies [1159e0f4b]
- Updated dependencies [ff2b4e2fb]
- Updated dependencies [0e285a443]
- Updated dependencies [5db46bd31]
- Updated dependencies [0cd65c571]
- @hyperlane-xyz/sdk@7.1.0
- @hyperlane-xyz/core@5.8.1
## 7.0.0
### Patch Changes

@ -1,11 +1,11 @@
{
"name": "@hyperlane-xyz/helloworld",
"description": "A basic skeleton of an Hyperlane app",
"version": "7.0.0",
"version": "7.1.0",
"dependencies": {
"@hyperlane-xyz/core": "5.8.0",
"@hyperlane-xyz/registry": "4.7.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/core": "5.8.1",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.1.0",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
"ethers": "^5.7.2"
},

@ -1,5 +1,24 @@
# @hyperlane-xyz/infra
## 7.1.0
### Minor Changes
- 5db46bd31: Implements persistent relayer for use in CLI
### Patch Changes
- Updated dependencies [6f2d50fbd]
- Updated dependencies [1159e0f4b]
- Updated dependencies [0e285a443]
- Updated dependencies [ff2b4e2fb]
- Updated dependencies [0e285a443]
- Updated dependencies [5db46bd31]
- Updated dependencies [0cd65c571]
- @hyperlane-xyz/sdk@7.1.0
- @hyperlane-xyz/utils@7.1.0
- @hyperlane-xyz/helloworld@7.1.0
## 7.0.0
### Minor Changes

@ -12,6 +12,7 @@ import {
import {
MetricAppContext,
routerMatchingList,
senderMatchingList,
warpRouteMatchingList,
} from '../../../src/config/agent/relayer.js';
import { ALL_KEY_ROLES, Role } from '../../../src/roles.js';
@ -20,14 +21,17 @@ import { getDomainId } from '../../registry.js';
import { environment } from './chains.js';
import { helloWorld } from './helloworld.js';
import aaveSenderAddresses from './misc-artifacts/aave-sender-addresses.json';
import merklyEthAddresses from './misc-artifacts/merkly-eth-addresses.json';
import merklyNftAddresses from './misc-artifacts/merkly-eth-addresses.json';
import merklyErc20Addresses from './misc-artifacts/merkly-eth-addresses.json';
import veloMessageModuleAddresses from './misc-artifacts/velo-message-module-addresses.json';
import veloTokenBridgeAddresses from './misc-artifacts/velo-token-bridge-addresses.json';
import {
mainnet3SupportedChainNames,
supportedChainNames,
} from './supportedChainNames.js';
import { validatorChainConfig } from './validators.js';
import merklyEthAddresses from './warp/artifacts/merkly-eth-addresses.json';
import merklyNftAddresses from './warp/artifacts/merkly-eth-addresses.json';
import merklyErc20Addresses from './warp/artifacts/merkly-eth-addresses.json';
import { WarpRouteIds } from './warp/warpIds.js';
// const releaseCandidateHelloworldMatchingList = routerMatchingList(
@ -375,6 +379,22 @@ const metricAppContextsGetter = (): MetricAppContext[] => {
name: 'merkly_nft',
matchingList: routerMatchingList(merklyNftAddresses),
},
{
name: 'velo_message_module',
matchingList: routerMatchingList(veloMessageModuleAddresses),
},
{
name: 'velo_token_bridge',
matchingList: routerMatchingList(veloTokenBridgeAddresses),
},
{
// https://github.com/bgd-labs/aave-delivery-infrastructure?tab=readme-ov-file#deployed-addresses
// We match on senders because the sender is always the same and
// well documented, while the recipient may be switched out and is
// more poorly documented.
name: 'aave',
matchingList: senderMatchingList(aaveSenderAddresses),
},
];
};
@ -409,7 +429,7 @@ const hyperlane: RootAgentConfig = {
rpcConsensusType: RpcConsensusType.Fallback,
docker: {
repo,
tag: '75d62ae-20241107-060707',
tag: '25a927d-20241114-171323',
},
gasPaymentEnforcement: gasPaymentEnforcement,
metricAppContextsGetter,
@ -443,7 +463,7 @@ const releaseCandidate: RootAgentConfig = {
rpcConsensusType: RpcConsensusType.Fallback,
docker: {
repo,
tag: '75d62ae-20241107-060707',
tag: '25a927d-20241114-171323',
},
// We're temporarily (ab)using the RC relayer as a way to increase
// message throughput.
@ -476,7 +496,7 @@ const neutron: RootAgentConfig = {
rpcConsensusType: RpcConsensusType.Fallback,
docker: {
repo,
tag: '75d62ae-20241107-060707',
tag: '25a927d-20241114-171323',
},
gasPaymentEnforcement,
metricAppContextsGetter,

@ -0,0 +1,38 @@
{
"ethereum": {
"sender": "0xEd42a7D8559a463722Ca4beD50E0Cc05a386b0e1"
},
"polygon": {
"sender": "0xF6B99959F0b5e79E1CC7062E12aF632CEb18eF0d"
},
"avalanche": {
"sender": "0x27FC7D54C893dA63C0AE6d57e1B2B13A70690928"
},
"arbitrum": {
"sender": "0xCbFB78a3Eeaa611b826E37c80E4126c8787D29f0"
},
"optimism": {
"sender": "0x48A9FE90bce5EEd790f3F4Ce192d1C0B351fd4Ca"
},
"bsc": {
"sender": "0x9d33ee6543C9b2C8c183b8fb58fB089266cffA19"
},
"base": {
"sender": "0x529467C76f234F2bD359d7ecF7c660A2846b04e2"
},
"metis": {
"sender": "0x6fDaFb26915ABD6065a1E1501a37Ac438D877f70"
},
"gnosis": {
"sender": "0x8Dc5310fc9D3D7D1Bb3D1F686899c8F082316c9F"
},
"scroll": {
"sender": "0x03073D3F4769f6b6604d616238fD6c636C99AD0A"
},
"polygonzkevm": {
"sender": "0xed7e0874526B9BB9E36C7e9472ed7ed324CEeE3B"
},
"celo": {
"sender": "0x4A5f4b29C0407E5Feb323305e121f563c7bC4d79"
}
}

@ -0,0 +1,8 @@
{
"optimism": {
"router": "0xF385603a12Be8b7B885222329c581FDD1C30071D"
},
"mode": {
"router": "0xF385603a12Be8b7B885222329c581FDD1C30071D"
}
}

@ -0,0 +1,8 @@
{
"optimism": {
"router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6"
},
"mode": {
"router": "0xA7287a56C01ac8Baaf8e7B662bDB41b10889C7A6"
}
}

@ -1,6 +1,7 @@
export enum WarpRouteIds {
Ancient8EthereumUSDC = 'USDC/ancient8-ethereum',
ArbitrumBaseBlastBscEthereumFraxtalLineaModeOptimismSeiTaikoZircuitEZETH = 'EZETH/arbitrum-base-blast-bsc-ethereum-fraxtal-linea-mode-optimism-sei-taiko-zircuit',
ArbitrumBaseEnduranceUSDC = 'USDC/arbitrum-base-endurance',
ArbitrumEthereumZircuitAMPHRETH = 'AMPHRETH/arbitrum-ethereum-zircuit',
ArbitrumNeutronEclip = 'ECLIP/arbitrum-neutron',
ArbitrumNeutronTIA = 'TIA/arbitrum-neutron',

@ -69,7 +69,7 @@ The warp-routes container
imagePullPolicy: IfNotPresent
env:
- name: LOG_FORMAT
value: pretty
value: json
command:
- ./node_modules/.bin/tsx
- ./typescript/infra/scripts/warp-routes/monitor/monitor-warp-route-balances.ts

@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/infra",
"description": "Infrastructure utilities for the Hyperlane Network",
"version": "7.0.0",
"version": "7.1.0",
"dependencies": {
"@arbitrum/sdk": "^3.0.0",
"@aws-sdk/client-iam": "^3.74.0",
@ -13,10 +13,10 @@
"@ethersproject/hardware-wallets": "^5.7.0",
"@ethersproject/providers": "^5.7.2",
"@google-cloud/secret-manager": "^5.5.0",
"@hyperlane-xyz/helloworld": "7.0.0",
"@hyperlane-xyz/registry": "4.10.0",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@hyperlane-xyz/helloworld": "7.1.0",
"@hyperlane-xyz/registry": "6.1.0",
"@hyperlane-xyz/sdk": "7.1.0",
"@hyperlane-xyz/utils": "7.1.0",
"@inquirer/prompts": "^5.3.8",
"@nomiclabs/hardhat-etherscan": "^3.0.3",
"@safe-global/api-kit": "1.3.0",

@ -191,9 +191,8 @@ export function withProtocol<T>(args: Argv<T>) {
export function withAgentRole<T>(args: Argv<T>) {
return args
.describe('role', 'agent roles')
.array('role')
.coerce('role', (role: string[]): Role[] => role.map(assertRole))
.describe('role', 'agent role')
.coerce('role', (role: string): Role => assertRole(role))
.demandOption('role')
.alias('r', 'role');
}
@ -206,11 +205,16 @@ export function withAgentRoles<T>(args: Argv<T>) {
.coerce('roles', (role: string[]): Role[] => role.map(assertRole))
.choices('roles', Object.values(Role))
// Ensure roles are unique
.coerce('roles', (roles: string[]) => Array.from(new Set(roles)))
.coerce('roles', (roles: Role[]) => Array.from(new Set(roles)))
.alias('r', 'roles')
.alias('role', 'roles')
);
}
export function withAgentRolesRequired<T>(args: Argv<T>) {
return withAgentRoles(args).demandOption('roles');
}
export function withKeyRoleAndChain<T>(args: Argv<T>) {
return args
.describe('role', 'key role')

@ -11,7 +11,7 @@ import { HelmCommand } from '../../src/utils/helm.js';
import {
assertCorrectKubeContext,
getArgs,
withAgentRole,
withAgentRolesRequired,
withChains,
withContext,
} from '../agent-utils.js';
@ -70,21 +70,23 @@ export class AgentCli {
protected async init() {
if (this.initialized) return;
const argv = await withChains(withAgentRole(withContext(getArgs())))
const argv = await withChains(
withAgentRolesRequired(withContext(getArgs())),
)
.describe('dry-run', 'Run through the steps without making any changes')
.boolean('dry-run').argv;
if (
argv.chains &&
argv.chains.length > 0 &&
!argv.role.includes(Role.Validator)
!argv.roles.includes(Role.Validator)
) {
console.warn('Chain argument applies to validator role only. Ignoring.');
}
const { envConfig, agentConfig } = await getConfigsBasedOnArgs(argv);
await assertCorrectKubeContext(envConfig);
this.roles = argv.role;
this.roles = argv.roles;
this.envConfig = envConfig;
this.agentConfig = agentConfig;
this.dryRun = argv.dryRun || false;

@ -1,6 +1,5 @@
import { createAgentKeysIfNotExists } from '../src/agents/key-utils.js';
import { getAgentConfigsBasedOnArgs } from './agent-utils.js';
import { createAgentKeysIfNotExists } from '../../src/agents/key-utils.js';
import { getAgentConfigsBasedOnArgs } from '../agent-utils.js';
async function main() {
const { agentConfig } = await getAgentConfigsBasedOnArgs();

@ -1,6 +1,5 @@
import { deleteAgentKeys } from '../src/agents/key-utils.js';
import { getAgentConfigsBasedOnArgs } from './agent-utils.js';
import { deleteAgentKeys } from '../../src/agents/key-utils.js';
import { getAgentConfigsBasedOnArgs } from '../agent-utils.js';
async function main() {
const { agentConfig } = await getAgentConfigsBasedOnArgs();

@ -1,7 +1,6 @@
import { getAllCloudAgentKeys } from '../src/agents/key-utils.js';
import { getArgs, withContext, withProtocol } from './agent-utils.js';
import { getConfigsBasedOnArgs } from './core-utils.js';
import { getAllCloudAgentKeys } from '../../src/agents/key-utils.js';
import { getArgs, withContext, withProtocol } from '../agent-utils.js';
import { getConfigsBasedOnArgs } from '../core-utils.js';
async function main() {
const argv = await withProtocol(withContext(getArgs())).argv;

@ -0,0 +1,38 @@
import { getCloudAgentKey } from '../../src/agents/key-utils.js';
import {
getArgs,
withAgentRole,
withContext,
withProtocol,
} from '../agent-utils.js';
import { getConfigsBasedOnArgs } from '../core-utils.js';
async function main() {
const argv = await withAgentRole(withContext(getArgs())).argv;
const { agentConfig } = await getConfigsBasedOnArgs(argv);
// As a (very rudimentary) security precaution, we don't print the private key directly to
// the console if this script is ran directly.
// We only write the private key to the console if it is not a tty, e.g. if
// this is being called in a subshell or piped to another command.
//
// E.g. this will print the private key:
// $ echo `yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer`
// or this too:
// $ echo $(yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer)
// and even this:
// $ yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer | cat
//
// But this will not print the private key directly to the shell:
// $ yarn tsx infra/scripts/keys/get-key.ts -e mainnet3 --role deployer
if (process.stdout.isTTY) {
console.log('<omitted in tty, use in subshell>');
} else {
const key = getCloudAgentKey(agentConfig, argv.role);
await key.fetch();
console.log(key.privateKey);
}
}
main().catch(console.error);

@ -1,11 +1,10 @@
import { AccountConfig, InterchainAccount } from '@hyperlane-xyz/sdk';
import { Address, eqAddress, isZeroishAddress } from '@hyperlane-xyz/utils';
import { chainsToSkip } from '../src/config/chain.js';
import { isEthereumProtocolChain } from '../src/utils/utils.js';
import { getArgs as getEnvArgs, withChains } from './agent-utils.js';
import { getEnvironmentConfig, getHyperlaneCore } from './core-utils.js';
import { chainsToSkip } from '../../src/config/chain.js';
import { isEthereumProtocolChain } from '../../src/utils/utils.js';
import { getArgs as getEnvArgs, withChains } from '../agent-utils.js';
import { getEnvironmentConfig, getHyperlaneCore } from '../core-utils.js';
function getArgs() {
return withChains(getEnvArgs())

@ -3,7 +3,7 @@ import {
getArgs,
withContext,
withKeyRoleAndChain,
} from './agent-utils.js';
} from '../agent-utils.js';
async function rotateKey() {
const argv = await withContext(withKeyRoleAndChain(getArgs())).argv;

@ -3,7 +3,7 @@ import {
getArgs,
withContext,
withKeyRoleAndChain,
} from './agent-utils.js';
} from '../agent-utils.js';
async function rotateKey() {
const argv = await withKeyRoleAndChain(withContext(getArgs())).argv;

@ -46,11 +46,18 @@ async function getWarpRouteIdsInteractive() {
value: id,
}));
const selection = await checkbox({
message: 'Select Warp Route IDs to deploy',
choices,
pageSize: 30,
});
let selection: WarpRouteIds[] = [];
while (!selection.length) {
selection = await checkbox({
message: 'Select Warp Route IDs to deploy',
choices,
pageSize: 30,
});
if (!selection.length) {
console.log('Please select at least one Warp Route ID');
}
}
return selection;
}

@ -74,17 +74,23 @@ export function updateTokenBalanceMetrics(
};
warpRouteTokenBalance.labels(metrics).set(balanceInfo.balance);
logger.info('Wallet balance updated for token', {
labels: metrics,
balance: balanceInfo.balance,
});
logger.info(
{
labels: metrics,
balance: balanceInfo.balance,
},
'Wallet balance updated for token',
);
if (balanceInfo.valueUSD) {
warpRouteCollateralValue.labels(metrics).set(balanceInfo.valueUSD);
logger.info('Wallet balance updated for token', {
labels: metrics,
valueUSD: balanceInfo.valueUSD,
});
logger.info(
{
labels: metrics,
valueUSD: balanceInfo.valueUSD,
},
'Wallet value updated for token',
);
}
}

@ -13,7 +13,7 @@ import {
TokenStandard,
WarpCore,
} from '@hyperlane-xyz/sdk';
import { ProtocolType, objMap, objMerge } from '@hyperlane-xyz/utils';
import { ProtocolType, objMap, objMerge, sleep } from '@hyperlane-xyz/utils';
import { getWarpCoreConfig } from '../../../config/registry.js';
import {
@ -78,7 +78,7 @@ async function pollAndUpdateWarpRouteMetrics(
apiKey: await getCoinGeckoApiKey(),
});
setInterval(async () => {
while (true) {
await tryFn(async () => {
await Promise.all(
warpCore.tokens.map((token) =>
@ -86,7 +86,8 @@ async function pollAndUpdateWarpRouteMetrics(
),
);
}, 'Updating warp route metrics');
}, checkFrequency);
await sleep(checkFrequency);
}
}
// Updates the metrics for a single token in a warp route.
@ -246,9 +247,7 @@ async function getCoinGeckoApiKey(): Promise<string | undefined> {
return apiKey;
}
main()
.then(logger.info)
.catch((err) => {
logger.error('Error in main', err);
process.exit(1);
});
main().catch((err) => {
logger.error('Error in main:', err);
process.exit(1);
});

@ -242,6 +242,18 @@ export function routerMatchingList(
return matchingList(routers);
}
// Create a matching list for the given senders to any destination or recipient
export function senderMatchingList(
senders: ChainMap<{ sender: Address }>,
): MatchingList {
return Object.entries(senders).map(([chain, { sender }]) => ({
originDomain: getDomainId(chain),
senderAddress: addressToBytes32(sender),
destinationDomain: '*',
recipientAddress: '*',
}));
}
// Create a matching list for the given contract addresses
export function matchingList<F extends HyperlaneFactories>(
addressesMap: HyperlaneAddressesMap<F>,

@ -22,7 +22,7 @@ export class WarpRouteMonitorHelmManager extends HelmManager {
return {
image: {
repository: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: '6cd61f1-20241112-111341',
tag: '7544b99-20241119-162215',
},
warpRouteId: this.warpRouteId,
fullnameOverride: this.helmReleaseName,

@ -1,5 +1,22 @@
# @hyperlane-xyz/sdk
## 7.1.0
### Minor Changes
- 6f2d50fbd: Updated Fraxtal set to include Superlane validators, updated Flow set
- 1159e0f4b: Enroll new validators for alephzeroevmmainnet, chilizmainnet, flowmainnet, immutablezkevmmainnet, metal, polynomialfi, rarichain, rootstockmainnet, superpositionmainnet, flame, prom, inevm.
- ff2b4e2fb: Added helpers to Token and token adapters to get bridged supply of tokens"
- 0e285a443: Add a validateZodResult util function
- 5db46bd31: Implements persistent relayer for use in CLI
- 0cd65c571: Add chainMetadataToCosmosChain function
### Patch Changes
- Updated dependencies [0e285a443]
- @hyperlane-xyz/utils@7.1.0
- @hyperlane-xyz/core@5.8.1
## 7.0.0
### Major Changes

@ -1,14 +1,15 @@
{
"name": "@hyperlane-xyz/sdk",
"description": "The official SDK for the Hyperlane Network",
"version": "7.0.0",
"version": "7.1.0",
"dependencies": {
"@arbitrum/sdk": "^4.0.0",
"@aws-sdk/client-s3": "^3.74.0",
"@chain-registry/types": "^0.50.14",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@cosmjs/stargate": "^0.32.4",
"@hyperlane-xyz/core": "5.8.0",
"@hyperlane-xyz/utils": "7.0.0",
"@hyperlane-xyz/core": "5.8.1",
"@hyperlane-xyz/utils": "7.1.0",
"@safe-global/api-kit": "1.3.0",
"@safe-global/protocol-kit": "1.3.0",
"@safe-global/safe-deployments": "1.37.8",
@ -19,7 +20,7 @@
"cross-fetch": "^3.1.5",
"ethers": "^5.7.2",
"pino": "^8.19.0",
"viem": "^2.21.40",
"viem": "^2.21.45",
"zod": "^3.21.2"
},
"devDependencies": {

@ -350,11 +350,12 @@ export const defaultMultisigConfigs: ChainMap<MultisigConfig> = {
},
flowmainnet: {
threshold: 2,
threshold: 3,
validators: [
'0xe132235c958ca1f3f24d772e5970dd58da4c0f6e',
'0xcf0211fafbb91fd9d06d7e306b30032dc3a1934f', // merkly
'0x4f977a59fdc2d9e39f6d780a84d5b4add1495a36', // mitosis
'0x14ADB9e3598c395Fe3290f3ba706C3816Aa78F59', // flow foundation
],
},
@ -364,11 +365,14 @@ export const defaultMultisigConfigs: ChainMap<MultisigConfig> = {
},
fraxtal: {
threshold: 2,
threshold: 4,
validators: [
'0x4bce180dac6da60d0f3a2bdf036ffe9004f944c1',
'0xcf0211fafbb91fd9d06d7e306b30032dc3a1934f', // merkly
'0x25b3a88f7cfd3c9f7d7e32b295673a16a6ddbd91', // luganodes
'0x0d4c1394a255568ec0ecd11795B28D1BdA183Ca4', // tessellated (superlane)
'0x1c3C3013B863Cf666499Da1A61949AE396E3Ab82', // enigma (superlane)
'0x573e960e07ad74ea2c5f1e3c31b2055994b12797', // imperator (superlane)
'0x14d0B24d3a8F3aAD17DB4b62cBcEC12821c98Cb3', // bware (superlane)
'0x25b3a88f7cfd3c9f7d7e32b295673a16a6ddbd91', // luganodes (superlane)
],
},

@ -26,7 +26,6 @@ export {
testCosmosChain,
testSealevelChain,
} from './consts/testChains.js';
export { randomAddress } from './test/testUtils.js';
export {
attachAndConnectContracts,
attachContracts,
@ -329,6 +328,7 @@ export {
SmartProviderOptions,
} from './providers/SmartProvider/types.js';
export { CallData } from './providers/transactions/types.js';
export { randomAddress } from './test/testUtils.js';
export { SubmitterMetadataSchema } from './providers/transactions/submitter/schemas.js';
export { TxSubmitterInterface } from './providers/transactions/submitter/TxSubmitterInterface.js';
@ -370,6 +370,10 @@ export { EV5TxTransformerInterface } from './providers/transactions/transformer/
export { EV5InterchainAccountTxTransformerPropsSchema } from './providers/transactions/transformer/ethersV5/schemas.js';
export { EV5InterchainAccountTxTransformerProps } from './providers/transactions/transformer/ethersV5/types.js';
export {
chainMetadataToCosmosChain,
chainMetadataToViemChain,
} from './metadata/chainMetadataConversion.js';
export {
EvmGasRouterAdapter,
EvmRouterAdapter,
@ -503,7 +507,6 @@ export {
getSealevelAccountDataSchema,
} from './utils/sealevelSerialization.js';
export { getChainIdFromTxs } from './utils/transactions.js';
export { chainMetadataToViemChain } from './utils/viem.js';
export {
FeeConstantConfig,
RouteBlacklist,
@ -533,7 +536,7 @@ export {
isSyntheticRebaseConfig,
isTokenMetadata,
} from './token/schemas.js';
export { isCompliant } from './utils/schemas.js';
export { isCompliant, validateZodResult } from './utils/schemas.js';
export {
canProposeSafeTransactions,

@ -0,0 +1,98 @@
import type { AssetList, Chain as CosmosChain } from '@chain-registry/types';
import { Chain, defineChain } from 'viem';
import { test1 } from '../consts/testChains.js';
import {
ChainMetadata,
getChainIdNumber,
} from '../metadata/chainMetadataTypes.js';
export function chainMetadataToViemChain(metadata: ChainMetadata): Chain {
return defineChain({
id: getChainIdNumber(metadata),
name: metadata.displayName || metadata.name,
network: metadata.name,
nativeCurrency: metadata.nativeToken || test1.nativeToken!,
rpcUrls: {
public: { http: [metadata.rpcUrls[0].http] },
default: { http: [metadata.rpcUrls[0].http] },
},
blockExplorers: metadata.blockExplorers?.length
? {
default: {
name: metadata.blockExplorers[0].name,
url: metadata.blockExplorers[0].url,
},
}
: undefined,
testnet: !!metadata.isTestnet,
});
}
export function chainMetadataToCosmosChain(metadata: ChainMetadata): {
chain: CosmosChain;
assets: AssetList;
} {
const {
name,
displayName,
chainId,
rpcUrls,
restUrls,
isTestnet,
nativeToken,
bech32Prefix,
slip44,
} = metadata;
if (!nativeToken) throw new Error(`Missing native token for ${name}`);
const chain: CosmosChain = {
chain_name: name,
chain_type: 'cosmos',
status: 'live',
network_type: isTestnet ? 'testnet' : 'mainnet',
pretty_name: displayName || name,
chain_id: chainId as string,
bech32_prefix: bech32Prefix!,
slip44: slip44!,
apis: {
rpc: [{ address: rpcUrls[0].http, provider: displayName || name }],
rest: restUrls
? [{ address: restUrls[0].http, provider: displayName || name }]
: [],
},
fees: {
fee_tokens: [{ denom: 'token' }],
},
staking: {
staking_tokens: [{ denom: 'stake' }],
},
};
const assets: AssetList = {
chain_name: name,
assets: [
{
description: `The native token of ${displayName || name} chain.`,
denom_units: [{ denom: 'token', exponent: nativeToken.decimals }],
base: 'token',
name: 'token',
display: 'token',
symbol: 'token',
type_asset: 'sdk.coin',
},
{
description: `The native token of ${displayName || name} chain.`,
denom_units: [{ denom: 'token', exponent: nativeToken.decimals }],
base: 'stake',
name: 'stake',
display: 'stake',
symbol: 'stake',
type_asset: 'sdk.coin',
},
],
};
return { chain, assets };
}

@ -40,6 +40,7 @@ import {
} from './adapters/CosmosTokenAdapter.js';
import {
EvmHypCollateralAdapter,
EvmHypCollateralFiatAdapter,
EvmHypNativeAdapter,
EvmHypSyntheticAdapter,
EvmHypXERC20Adapter,
@ -192,13 +193,16 @@ export class Token implements IToken {
});
} else if (
standard === TokenStandard.EvmHypCollateral ||
standard === TokenStandard.EvmHypCollateralFiat ||
standard === TokenStandard.EvmHypOwnerCollateral ||
standard === TokenStandard.EvmHypRebaseCollateral
) {
return new EvmHypCollateralAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (standard === TokenStandard.EvmHypCollateralFiat) {
return new EvmHypCollateralFiatAdapter(chainName, multiProvider, {
token: addressOrDenom,
});
} else if (
standard === TokenStandard.EvmHypSynthetic ||
standard === TokenStandard.EvmHypSyntheticRebase

@ -261,9 +261,7 @@ export class EvmHypCollateralAdapter
return this.wrappedTokenAddress!;
}
protected async getWrappedTokenAdapter(): Promise<
ITokenAdapter<PopulatedTransaction>
> {
protected async getWrappedTokenAdapter(): Promise<EvmTokenAdapter> {
return new EvmTokenAdapter(this.chainName, this.multiProvider, {
token: await this.getWrappedTokenAddress(),
});
@ -304,6 +302,21 @@ export class EvmHypCollateralAdapter
}
}
export class EvmHypCollateralFiatAdapter
extends EvmHypCollateralAdapter
implements IHypTokenAdapter<PopulatedTransaction>
{
/**
* Note this may be inaccurate, as this returns the total supply
* of the fiat token, which may be used by other bridges.
* However this is the best we can do with a simple view call.
*/
override async getBridgedSupply(): Promise<bigint> {
const wrapped = await this.getWrappedTokenAdapter();
return wrapped.getTotalSupply();
}
}
// Interacts with HypXERC20Lockbox contracts
export class EvmHypXERC20LockboxAdapter
extends EvmHypCollateralAdapter

@ -1,6 +1,20 @@
import { z } from 'zod';
import { SafeParseReturnType, z } from 'zod';
import { rootLogger } from '@hyperlane-xyz/utils';
export function isCompliant<S extends Zod.Schema>(schema: S) {
return (config: unknown): config is z.infer<S> =>
schema.safeParse(config).success;
}
export function validateZodResult<T>(
result: SafeParseReturnType<T, T>,
desc: string = 'config',
): T {
if (!result.success) {
rootLogger.warn(`Invalid ${desc}`, result.error);
throw new Error(`Invalid desc: ${result.error.toString()}`);
} else {
return result.data;
}
}

@ -1,29 +0,0 @@
import { Chain, defineChain } from 'viem';
import { test1 } from '../consts/testChains.js';
import {
ChainMetadata,
getChainIdNumber,
} from '../metadata/chainMetadataTypes.js';
export function chainMetadataToViemChain(metadata: ChainMetadata): Chain {
return defineChain({
id: getChainIdNumber(metadata),
name: metadata.displayName || metadata.name,
network: metadata.name,
nativeCurrency: metadata.nativeToken || test1.nativeToken!,
rpcUrls: {
public: { http: [metadata.rpcUrls[0].http] },
default: { http: [metadata.rpcUrls[0].http] },
},
blockExplorers: metadata.blockExplorers?.length
? {
default: {
name: metadata.blockExplorers[0].name,
url: metadata.blockExplorers[0].url,
},
}
: undefined,
testnet: !!metadata.isTestnet,
});
}

@ -1,5 +1,11 @@
# @hyperlane-xyz/utils
## 7.1.0
### Minor Changes
- 0e285a443: Add an isRelativeUrl function
## 7.0.0
### Major Changes

@ -1,7 +1,7 @@
{
"name": "@hyperlane-xyz/utils",
"description": "General utilities and types for the Hyperlane network",
"version": "7.0.0",
"version": "7.1.0",
"dependencies": {
"@cosmjs/encoding": "^0.32.4",
"@solana/web3.js": "^1.95.4",

@ -170,7 +170,7 @@ export {
TokenCaip19Id,
WithAddress,
} from './types.js';
export { isHttpsUrl, isUrl } from './url.js';
export { isHttpsUrl, isRelativeUrl, isUrl } from './url.js';
export { assert } from './validation.js';
export { BaseValidator, ValidatorConfig } from './validator.js';
export { tryParseJsonOrYaml } from './yaml.js';

@ -0,0 +1,32 @@
import { expect } from 'chai';
import { isHttpsUrl, isRelativeUrl, isUrl } from './url.js';
describe('URL Utilities', () => {
it('isUrl', () => {
expect(isUrl(undefined)).to.be.false;
expect(isUrl(null)).to.be.false;
expect(isUrl('')).to.be.false;
expect(isUrl('foobar')).to.be.false;
expect(isUrl('https://hyperlane.xyz')).to.be.true;
});
it('isHttpsUrl', () => {
expect(isHttpsUrl(undefined)).to.be.false;
expect(isHttpsUrl(null)).to.be.false;
expect(isHttpsUrl('')).to.be.false;
expect(isHttpsUrl('foobar')).to.be.false;
expect(isHttpsUrl('http://hyperlane.xyz')).to.be.false;
expect(isHttpsUrl('https://hyperlane.xyz')).to.be.true;
});
it('isRelativeUrl', () => {
expect(isRelativeUrl(undefined)).to.be.false;
expect(isRelativeUrl(null)).to.be.false;
expect(isRelativeUrl('')).to.be.false;
expect(isRelativeUrl('foobar')).to.be.false;
expect(isRelativeUrl('https://hyperlane.xyz')).to.be.false;
expect(isRelativeUrl('/foobar')).to.be.true;
expect(isRelativeUrl('/foo/bar', 'https://hyperlane.xyz')).to.be.true;
});
});

@ -1,5 +1,6 @@
export function isUrl(value: string) {
export function isUrl(value?: string | null) {
try {
if (!value) return false;
const url = new URL(value);
return !!url.hostname;
} catch (error) {
@ -7,11 +8,22 @@ export function isUrl(value: string) {
}
}
export function isHttpsUrl(value: string) {
export function isHttpsUrl(value?: string | null) {
try {
if (!value) return false;
const url = new URL(value);
return url.protocol === 'https:';
} catch (error) {
return false;
}
}
export function isRelativeUrl(value?: string | null, base?: string): boolean {
try {
if (!value || !value.startsWith('/')) return false;
const url = new URL(value, base || 'https://hyperlane.xyz');
return !!url.pathname;
} catch {
return false;
}
}

@ -1,18 +1,20 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"plugins": ["react", "react-hooks", "@typescript-eslint"],
"plugins": ["react", "react-hooks"],
"rules": {
// TODO use utils rootLogger in widgets lib
"no-console": ["off"],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
},
"settings": {
"react": {
"version": "18",
"defaultVersion": "18"
}
}
}

@ -10,6 +10,11 @@ const config: StorybookConfig = {
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
refs: {
'@chakra-ui/react': {
disable: true,
},
},
framework: {
name: '@storybook/react-vite',
options: {},

@ -1,5 +1,30 @@
# @hyperlane-xyz/widgets
## 7.1.0
### Minor Changes
- 0cd65c571: Add multi-protocol wallet integration hooks and types
- 186663505: New Icons
Updated modal with new props
Updated storybook for modal and icon list
- 0e285a443: Add various utility hooks: useIsSsr, useTimeout, useDebounce, useInterval
- 92b5fe777: Props and style update: IconButton and Tooltip
New Icons: XCircleIcon and SwapIcon
### Patch Changes
- 794501ba6: Prevent propagation of form submissions from ChainSearchMenu
- Updated dependencies [6f2d50fbd]
- Updated dependencies [1159e0f4b]
- Updated dependencies [0e285a443]
- Updated dependencies [ff2b4e2fb]
- Updated dependencies [0e285a443]
- Updated dependencies [5db46bd31]
- Updated dependencies [0cd65c571]
- @hyperlane-xyz/sdk@7.1.0
- @hyperlane-xyz/utils@7.1.0
## 7.0.0
### Patch Changes

@ -1,20 +1,32 @@
{
"name": "@hyperlane-xyz/widgets",
"description": "Common react components for Hyperlane projects",
"version": "7.0.0",
"version": "7.1.0",
"peerDependencies": {
"react": "^18",
"react-dom": "^18"
},
"dependencies": {
"@cosmos-kit/react": "^2.18.0",
"@headlessui/react": "^2.1.8",
"@hyperlane-xyz/sdk": "7.0.0",
"@hyperlane-xyz/utils": "7.0.0",
"@hyperlane-xyz/sdk": "7.1.0",
"@hyperlane-xyz/utils": "7.1.0",
"@interchain-ui/react": "^1.23.28",
"@rainbow-me/rainbowkit": "^2.2.0",
"@solana/wallet-adapter-react": "^0.15.32",
"@solana/wallet-adapter-react-ui": "^0.9.31",
"@solana/web3.js": "^1.95.4",
"clsx": "^2.1.1",
"react-tooltip": "^5.28.0"
"react-tooltip": "^5.28.0",
"viem": "^2.21.41",
"wagmi": "^2.12.26"
},
"devDependencies": {
"@hyperlane-xyz/registry": "4.7.0",
"@chakra-ui/react": "^2.8.2",
"@cosmjs/cosmwasm-stargate": "^0.32.4",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hyperlane-xyz/registry": "6.1.0",
"@storybook/addon-essentials": "^7.6.14",
"@storybook/addon-interactions": "^7.6.14",
"@storybook/addon-links": "^7.6.14",
@ -23,6 +35,7 @@
"@storybook/react": "^7.6.14",
"@storybook/react-vite": "^7.6.14",
"@storybook/test": "^7.6.14",
"@tanstack/react-query": "^5.59.20",
"@types/node": "^18.11.18",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
@ -35,6 +48,7 @@
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-storybook": "^0.6.15",
"framer-motion": "^10.16.4",
"postcss": "^8.4.21",
"prettier": "^2.8.8",
"react": "^18.2.0",
@ -72,7 +86,7 @@
"build:ts": "tsc",
"build:css": "tailwindcss -c ./tailwind.config.cjs -i ./src/styles.css -o ./dist/styles.css --minify",
"clean": "rm -rf ./dist ./cache ./storybook-static",
"lint": "eslint ./src --ext .ts",
"lint": "eslint ./src --ext '.ts,.tsx'",
"prettier": "prettier --write ./src",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"

@ -21,6 +21,7 @@ import { CopyButton } from '../components/CopyButton.js';
import { LinkButton } from '../components/LinkButton.js';
import { ChevronIcon } from '../icons/Chevron.js';
import { PlusIcon } from '../icons/Plus.js';
import { widgetLogger } from '../logger.js';
export interface ChainAddMenuProps {
chainMetadata: ChainMap<ChainMetadata>;
@ -143,7 +144,7 @@ function tryParseMetadataInput(
const result = ChainMetadataSchema.safeParse(parsed.data);
if (!result.success) {
console.error('Error validating chain config', result.error);
widgetLogger.error('Error validating chain config', result.error);
const firstIssue = result.error.issues[0];
return failure(`${firstIssue.path} => ${firstIssue.message}`);
}

@ -4,6 +4,7 @@ import type { IRegistry } from '@hyperlane-xyz/registry';
import { Circle } from '../icons/Circle.js';
import { QuestionMarkIcon } from '../icons/QuestionMark.js';
import { widgetLogger } from '../logger.js';
type SvgIcon = (props: {
width: number;
@ -41,8 +42,8 @@ export function ChainLogo({
registry
.getChainLogoUri(chainName)
.then((uri) => uri && setSvgLogos({ ...svgLogos, [chainName]: uri }))
.catch((err) => console.error(err));
}, [chainName, registry, svgLogos, Icon]);
.catch((err) => widgetLogger.error('Error fetching log uri', err));
}, [chainName, logoUri, registry, svgLogos, Icon]);
if (!uri && !Icon) {
return (

@ -89,7 +89,7 @@ export function ChainSearchMenu({
overrideChainMetadata,
);
return { mergedMetadata, listData: Object.values(mergedMetadata) };
}, [chainMetadata]);
}, [chainMetadata, overrideChainMetadata]);
const { ListComponent, searchFn, sortOptions, defaultSortState } =
useCustomizedListItems(customListItemField, defaultSortField);
@ -297,7 +297,7 @@ function useCustomizedListItems(
({ data }: { data: ChainMetadata<{ disabled?: boolean }> }) => (
<ChainListItem data={data} customField={customListItemField} />
),
[ChainListItem, customListItemField],
[customListItemField],
);
// Bind the custom field to the search function

@ -0,0 +1,48 @@
import React, { Component, PropsWithChildren, ReactNode } from 'react';
import { errorToString } from '@hyperlane-xyz/utils';
import { ErrorIcon } from '../icons/Error.js';
import { widgetLogger } from '../logger.js';
type Props = PropsWithChildren<{
supportLink?: ReactNode;
}>;
interface State {
error: any;
errorInfo: any;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error: any, errorInfo: any) {
this.setState({
error,
errorInfo,
});
widgetLogger.error('Error caught by error boundary', error, errorInfo);
}
render() {
const errorInfo = this.state.error || this.state.errorInfo;
if (errorInfo) {
const details = errorToString(errorInfo, 1000);
return (
<div className="htw-flex htw-h-screen htw-w-screen htw-items-center htw-justify-center htw-bg-gray-50">
<div className="htw-flex htw-flex-col htw-items-center htw-space-y-5">
<ErrorIcon width={80} height={80} />
<h1 className="htw-text-lg">Fatal Error Occurred</h1>
<div className="htw-max-w-2xl htw-text-sm">{details}</div>
{this.props.supportLink}
</div>
</div>
);
}
return this.props.children;
}
}

@ -102,12 +102,13 @@ export function SearchMenu<
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
e.stopPropagation();
if (results.length === 1) {
const item = results[0];
isEditMode ? onClickEditItem(item) : onClickItem(item);
}
},
[results, isEditMode],
[results, isEditMode, onClickEditItem, onClickItem],
);
useEffect(() => {
@ -254,6 +255,7 @@ function SortDropdown<SortBy extends string>({
buttonClassname="htw-flex htw-items-stretch hover:htw-bg-gray-100 active:htw-scale-95"
menuClassname="htw-py-1.5 htw-px-2 htw-flex htw-flex-col htw-gap-2 htw-text-sm htw-border htw-border-gray-100"
menuItems={options.map((o) => (
// eslint-disable-next-line react/jsx-key
<div
className="htw-rounded htw-p-1.5 hover:htw-bg-gray-200"
onClick={() => onSetSortBy(o)}
@ -298,7 +300,7 @@ function FilterDropdown<FilterState>({
(k) => !deepEquals(value[k], defaultValue[k]),
);
return modifiedKeys.map((k) => value[k]);
}, [value]);
}, [value, defaultValue]);
const hasFilters = filterValues.length > 0;
const onClear = () => {

@ -0,0 +1,38 @@
import React, { memo } from 'react';
import { ColorPalette } from '../color.js';
import { DefaultIconProps } from './types.js';
type Props = DefaultIconProps & {
direction?: 'horizontal' | 'vertical';
};
function _EllipsisIcon({
color = ColorPalette.Black,
width = 24,
height = 6,
direction = 'horizontal',
className,
...rest
}: Props) {
return (
<svg
width={width}
height={height}
viewBox="0 0 24 6"
xmlns="http://www.w3.org/2000/svg"
className={`${
direction === 'vertical' ? 'htw-rotate-90' : ''
} ${className}`}
{...rest}
>
<path
d="M3.42836 5.25C2.74638 5.25 2.09232 5.01295 1.61009 4.59099C1.12785 4.16903 0.856934 3.59674 0.856934 3C0.856934 2.40326 1.12785 1.83097 1.61009 1.40901C2.09232 0.987053 2.74638 0.75 3.42836 0.75C4.11035 0.75 4.7644 0.987053 5.24664 1.40901C5.72887 1.83097 5.99979 2.40326 5.99979 3C5.99979 3.59674 5.72887 4.16903 5.24664 4.59099C4.7644 5.01295 4.11035 5.25 3.42836 5.25ZM11.9998 5.25C11.3178 5.25 10.6638 5.01295 10.1815 4.59099C9.69928 4.16903 9.42836 3.59674 9.42836 3C9.42836 2.40326 9.69928 1.83097 10.1815 1.40901C10.6638 0.987053 11.3178 0.75 11.9998 0.75C12.6818 0.75 13.3358 0.987053 13.8181 1.40901C14.3003 1.83097 14.5712 2.40326 14.5712 3C14.5712 3.59674 14.3003 4.16903 13.8181 4.59099C13.3358 5.01295 12.6818 5.25 11.9998 5.25ZM20.5712 5.25C19.8892 5.25 19.2352 5.01295 18.7529 4.59099C18.2707 4.16903 17.9998 3.59674 17.9998 3C17.9998 2.40326 18.2707 1.83097 18.7529 1.40901C19.2352 0.987053 19.8892 0.75 20.5712 0.75C21.2532 0.75 21.9073 0.987053 22.3895 1.40901C22.8717 1.83097 23.1426 2.40326 23.1426 3C23.1426 3.59674 22.8717 4.16903 22.3895 4.59099C21.9073 5.01295 21.2532 5.25 20.5712 5.25Z"
fill={color}
/>
</svg>
);
}
export const EllipsisIcon = memo(_EllipsisIcon);

@ -0,0 +1,18 @@
import React, { memo } from 'react';
import { ColorPalette } from '../color.js';
import { DefaultIconProps } from './types.js';
function _Error({ color, ...rest }: DefaultIconProps) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" {...rest}>
<path
fill={color || ColorPalette.Black}
d="M24 34q.7 0 1.18-.47.47-.48.47-1.18t-.47-1.18q-.48-.47-1.18-.47t-1.18.47q-.47.48-.47 1.18t.47 1.18Q23.3 34 24 34Zm.15-7.65q.65 0 1.07-.42.43-.43.43-1.08V15.2q0-.65-.42-1.07-.43-.43-1.08-.43-.65 0-1.07.42-.43.43-.43 1.08v9.65q0 .65.42 1.07.43.43 1.08.43ZM24 44q-4.1 0-7.75-1.57-3.65-1.58-6.38-4.3-2.72-2.73-4.3-6.38Q4 28.1 4 23.95q0-4.1 1.57-7.75 1.58-3.65 4.3-6.35 2.73-2.7 6.38-4.28Q19.9 4 24.05 4q4.1 0 7.75 1.57 3.65 1.58 6.35 4.28 2.7 2.7 4.28 6.35Q44 19.85 44 24q0 4.1-1.57 7.75-1.58 3.65-4.28 6.38t-6.35 4.3Q28.15 44 24 44Zm.05-3q7.05 0 12-4.97T41 23.95q0-7.05-4.95-12T24 7q-7.05 0-12.03 4.95Q7 16.9 7 24q0 7.05 4.97 12.03Q16.95 41 24.05 41ZM24 24Z"
/>
</svg>
);
}
export const ErrorIcon = memo(_Error);

@ -0,0 +1,29 @@
import React, { memo } from 'react';
import { ColorPalette } from '../color.js';
import { DefaultIconProps } from './types.js';
function _LogoutIcon({
color = ColorPalette.Black,
width = 48,
height = 48,
...rest
}: DefaultIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
height={height}
width={width}
{...rest}
>
<path
fill={color}
d="M9 42q-1.2 0-2.1-.9Q6 40.2 6 39V9q0-1.2.9-2.1Q7.8 6 9 6h14.55v3H9v30h14.55v3Zm24.3-9.25-2.15-2.15 5.1-5.1h-17.5v-3h17.4l-5.1-5.1 2.15-2.15 8.8 8.8Z"
/>
</svg>
);
}
export const LogoutIcon = memo(_LogoutIcon);

@ -0,0 +1,30 @@
import React, { memo } from 'react';
import { ColorPalette } from '../color.js';
import { DefaultIconProps } from './types.js';
function _WarningIcon({
color = ColorPalette.Black,
width = 18,
height = 16,
...rest
}: DefaultIconProps) {
return (
<svg
width={width}
height={height}
viewBox="0 0 18 16"
xmlns="http://www.w3.org/2000/svg"
{...rest}
>
<path
d="M17.7385 12.8424L10.3944 1.37207C10.0899 0.896794 9.56445 0.609344 9.00031 0.609344C8.43613 0.609344 7.9106 0.896794 7.60622 1.37207L0.261512 12.8424C-0.0648531 13.3519 -0.0874155 13.999 0.203235 14.5299C0.493345 15.0604 1.05035 15.3907 1.65552 15.3907H16.3444C16.9496 15.3907 17.5066 15.0607 17.7967 14.5299C18.0875 13.999 18.0649 13.3519 17.7385 12.8424ZM8.07826 5.27148C8.07826 4.76708 8.48727 4.35837 8.99136 4.35837C9.49553 4.35837 9.9045 4.76708 9.9045 5.27148V9.18939C9.9045 9.69379 9.49553 10.1025 8.99136 10.1025C8.48727 10.1025 8.07826 9.69379 8.07826 9.18939V5.27148ZM8.97416 13.4096C8.34403 13.4096 7.83277 12.8986 7.83277 12.2682C7.83277 11.6377 8.34403 11.1268 8.97416 11.1268C9.60433 11.1268 10.1156 11.6377 10.1156 12.2682C10.1155 12.8986 9.60433 13.4096 8.97416 13.4096Z"
fill={color}
fillOpacity="0.8"
/>
</svg>
);
}
export const WarningIcon = memo(_WarningIcon);

@ -57,9 +57,9 @@ function _Web({ color = ColorPalette.Black, ...rest }: DefaultIconProps) {
/>
<path
fill={color}
fill-rule="evenodd"
fillRule="evenodd"
d="M28.84 14.57a14 14 0 1 0-14 14 14 14 0 0 0 14-14Zm-3 0a11 11 0 1 0-22 0 11 11 0 0 0 22 0Z"
clip-rule="evenodd"
clipRule="evenodd"
/>
</svg>
);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save