From 788673eaf810ebb2b5aea2a6e10dd6cb1da1a529 Mon Sep 17 00:00:00 2001 From: Ivan Frolov Date: Thu, 26 Mar 2026 20:43:41 -0400 Subject: [PATCH 1/5] feat(trusted-relayer): store relayers in iterable map --- omni-utils-derive/src/trusted_relayer.rs | 30 +- omni-utils/src/trusted_relayer.rs | 74 ++++- omni-utils/tests/trusted_relayer.rs | 378 +++++++++++++++++++++++ tests/test-contract-custom/src/lib.rs | 2 +- 4 files changed, 472 insertions(+), 12 deletions(-) diff --git a/omni-utils-derive/src/trusted_relayer.rs b/omni-utils-derive/src/trusted_relayer.rs index 2113874..b1dcd3d 100644 --- a/omni-utils-derive/src/trusted_relayer.rs +++ b/omni-utils-derive/src/trusted_relayer.rs @@ -150,7 +150,7 @@ fn gen_bypass_is_trusted(bypass_roles: &[Expr]) -> TokenStream2 { return true; } - ::omni_utils::trusted_relayer::tr_relayers_map() + ::omni_utils::trusted_relayer::tr_load_relayers() .get(account_id) .is_some_and(|state| { ::near_sdk::env::block_timestamp() >= state.activate_at.0 @@ -263,6 +263,32 @@ fn gen_public_methods( ) -> ::omni_utils::trusted_relayer::RelayerConfig { ::_tr_get_config(self) } + + #[must_use] + pub fn get_active_relayers( + &self, + from_index: Option, + limit: Option, + ) -> Vec<(::near_sdk::AccountId, ::omni_utils::trusted_relayer::RelayerState)> { + ::_tr_get_active_relayers( + self, + from_index, + limit, + ) + } + + #[must_use] + pub fn get_pending_relayers( + &self, + from_index: Option, + limit: Option, + ) -> Vec<(::near_sdk::AccountId, ::omni_utils::trusted_relayer::RelayerState)> { + ::_tr_get_pending_relayers( + self, + from_index, + limit, + ) + } } } } @@ -319,7 +345,7 @@ fn gen_method_bypass_guard(bypass_roles: &[Expr]) -> syn::Stmt { __caller.clone(), ) { ::near_sdk::require!( - ::omni_utils::trusted_relayer::tr_relayers_map() + ::omni_utils::trusted_relayer::tr_load_relayers() .get(&__caller) .is_some_and(|state| { ::near_sdk::env::block_timestamp() >= state.activate_at.0 diff --git a/omni-utils/src/trusted_relayer.rs b/omni-utils/src/trusted_relayer.rs index c74c53f..151fa16 100644 --- a/omni-utils/src/trusted_relayer.rs +++ b/omni-utils/src/trusted_relayer.rs @@ -1,11 +1,12 @@ use near_sdk::borsh::{self, BorshDeserialize}; use near_sdk::json_types::{U64, U128}; use near_sdk::serde_json::json; -use near_sdk::store::LookupMap; +use near_sdk::store::IterableMap; use near_sdk::{AccountId, NearToken, Promise, env, near, require}; const TR_CONFIG_KEY: &[u8] = b"__tr_config"; const TR_RELAYERS_PREFIX: &[u8] = b"__tr_relayers"; +const TR_RELAYERS_META_KEY: &[u8] = b"__tr_relayers_meta"; #[derive(Debug, Clone)] #[near(serializers = [json, borsh])] @@ -71,8 +72,25 @@ pub fn tr_save_config(config: &RelayerConfig) { ); } -pub fn tr_relayers_map() -> LookupMap { - LookupMap::new(TR_RELAYERS_PREFIX) +/// Load the relayers map, restoring iteration metadata from storage. +/// On first use (no metadata yet), creates a fresh empty map. +pub fn tr_load_relayers() -> IterableMap { + match env::storage_read(TR_RELAYERS_META_KEY) { + Some(bytes) => BorshDeserialize::try_from_slice(&bytes) + .unwrap_or_else(|_| env::panic_str("Failed to deserialize relayers map metadata")), + None => IterableMap::new(TR_RELAYERS_PREFIX), + } +} + +/// Flush pending writes and persist the relayers map metadata (length + prefixes) +/// so that future calls can restore iteration state. +pub fn tr_save_relayers(mut map: IterableMap) { + map.flush(); + env::storage_write( + TR_RELAYERS_META_KEY, + &borsh::to_vec(&map) + .unwrap_or_else(|_| env::panic_str("Failed to serialize relayers map metadata")), + ); } /// Trusted relayer staking support for NEAR contracts. @@ -87,14 +105,14 @@ pub trait TrustedRelayer { return true; } - tr_relayers_map() + tr_load_relayers() .get(account_id) .is_some_and(|state| env::block_timestamp() >= state.activate_at.0) } fn _tr_apply(&mut self) { let account_id = env::predecessor_account_id(); - let mut relayers = tr_relayers_map(); + let mut relayers = tr_load_relayers(); require!( relayers.get(&account_id).is_none(), @@ -123,6 +141,8 @@ pub trait TrustedRelayer { }, ); + tr_save_relayers(relayers); + TrustedRelayerEvent::RelayerApplyEvent { account_id: account_id.clone(), stake: config.stake_required, @@ -137,7 +157,7 @@ pub trait TrustedRelayer { fn _tr_resign(&mut self) -> Promise { let account_id = env::predecessor_account_id(); - let mut relayers = tr_relayers_map(); + let mut relayers = tr_load_relayers(); let state = relayers .remove(&account_id) @@ -148,6 +168,8 @@ pub trait TrustedRelayer { "Relayer is not active yet" ); + tr_save_relayers(relayers); + TrustedRelayerEvent::RelayerResignEvent { account_id: account_id.clone(), stake: state.stake, @@ -158,12 +180,14 @@ pub trait TrustedRelayer { } fn _tr_reject(&mut self, account_id: AccountId) -> Promise { - let mut relayers = tr_relayers_map(); + let mut relayers = tr_load_relayers(); let state = relayers .remove(&account_id) .unwrap_or_else(|| env::panic_str("Relayer application not found")); + tr_save_relayers(relayers); + TrustedRelayerEvent::RelayerRejectEvent { account_id: account_id.clone(), stake: state.stake, @@ -181,14 +205,14 @@ pub trait TrustedRelayer { } fn _tr_get_application(&self, account_id: &AccountId) -> Option { - tr_relayers_map() + tr_load_relayers() .get(account_id) .filter(|state| env::block_timestamp() < state.activate_at.0) .cloned() } fn _tr_get_stake(&self, account_id: &AccountId) -> Option { - tr_relayers_map() + tr_load_relayers() .get(account_id) .filter(|state| env::block_timestamp() >= state.activate_at.0) .map(|state| U128(state.stake.as_yoctonear())) @@ -197,4 +221,36 @@ pub trait TrustedRelayer { fn _tr_get_config(&self) -> RelayerConfig { tr_load_config() } + + fn _tr_get_active_relayers( + &self, + from_index: Option, + limit: Option, + ) -> Vec<(AccountId, RelayerState)> { + let relayers = tr_load_relayers(); + let now = env::block_timestamp(); + relayers + .iter() + .filter(|(_, state)| now >= state.activate_at.0) + .skip(from_index.unwrap_or(0) as usize) + .take(limit.unwrap_or(100) as usize) + .map(|(id, state)| (id.clone(), state.clone())) + .collect() + } + + fn _tr_get_pending_relayers( + &self, + from_index: Option, + limit: Option, + ) -> Vec<(AccountId, RelayerState)> { + let relayers = tr_load_relayers(); + let now = env::block_timestamp(); + relayers + .iter() + .filter(|(_, state)| now < state.activate_at.0) + .skip(from_index.unwrap_or(0) as usize) + .take(limit.unwrap_or(100) as usize) + .map(|(id, state)| (id.clone(), state.clone())) + .collect() + } } diff --git a/omni-utils/tests/trusted_relayer.rs b/omni-utils/tests/trusted_relayer.rs index 0fc7441..87051bb 100644 --- a/omni-utils/tests/trusted_relayer.rs +++ b/omni-utils/tests/trusted_relayer.rs @@ -1388,3 +1388,381 @@ async fn test_custom_generated_public_methods_exist() { .expect("Admin should be able to set config") .assert_success(); } + +#[tokio::test] +async fn test_get_active_relayers_empty() { + let env = get_env().await; + let (contract_id, _) = deploy_contract(env).await; + + let result: Data> = Contract(contract_id) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert!(result.data.is_empty(), "Should return empty list when no relayers exist"); +} + +#[tokio::test] +async fn test_get_pending_relayers_empty() { + let env = get_env().await; + let (contract_id, _) = deploy_contract(env).await; + + let result: Data> = Contract(contract_id) + .call_function("get_pending_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert!(result.data.is_empty(), "Should return empty list when no relayers exist"); +} + +#[tokio::test] +async fn test_get_pending_relayers_returns_pending() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + let (relayer_id, relayer_signer) = create_account(env, 20).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + // Long waiting period so the relayer stays pending + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, u64::MAX).await; + + // Apply + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(relayer_id.clone(), relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + + // Should appear in pending + let pending: Data> = Contract(contract_id.clone()) + .call_function("get_pending_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(pending.data.len(), 1, "Should have one pending relayer"); + assert_eq!( + pending.data[0].0, + relayer_id.to_string(), + "Pending relayer account_id should match" + ); + + // Should NOT appear in active + let active: Data> = Contract(contract_id) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert!(active.data.is_empty(), "Should have no active relayers while pending"); +} + +#[tokio::test] +async fn test_get_active_relayers_returns_active() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + let (relayer_id, relayer_signer) = create_account(env, 20).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + // 0 waiting period → instantly active + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, 0).await; + + // Apply + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(relayer_id.clone(), relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + + // Should appear in active + let active: Data> = Contract(contract_id.clone()) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(active.data.len(), 1, "Should have one active relayer"); + assert_eq!( + active.data[0].0, + relayer_id.to_string(), + "Active relayer account_id should match" + ); + + // Should NOT appear in pending + let pending: Data> = Contract(contract_id) + .call_function("get_pending_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert!(pending.data.is_empty(), "Should have no pending relayers when instantly active"); +} + +#[tokio::test] +async fn test_get_active_relayers_multiple_with_pagination() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + // 0 waiting period → instantly active + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, 0).await; + + // Apply with 3 relayers + let mut relayer_ids = Vec::new(); + for _ in 0..3 { + let (relayer_id, relayer_signer) = create_account(env, 20).await; + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(relayer_id.clone(), relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + relayer_ids.push(relayer_id.to_string()); + } + + // Get all active relayers (no pagination args) + let all: Data> = Contract(contract_id.clone()) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(all.data.len(), 3, "Should have 3 active relayers"); + + // Paginate: first 2 + let page1: Data> = Contract(contract_id.clone()) + .call_function( + "get_active_relayers", + json!({ "from_index": 0, "limit": 2 }), + ) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(page1.data.len(), 2, "First page should have 2 relayers"); + + // Paginate: next page starting from index 2 + let page2: Data> = Contract(contract_id.clone()) + .call_function( + "get_active_relayers", + json!({ "from_index": 2, "limit": 2 }), + ) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(page2.data.len(), 1, "Second page should have 1 relayer"); + + // Paginate: beyond range + let page3: Data> = Contract(contract_id) + .call_function( + "get_active_relayers", + json!({ "from_index": 10, "limit": 2 }), + ) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert!(page3.data.is_empty(), "Page beyond range should be empty"); +} + +#[tokio::test] +async fn test_get_pending_relayers_multiple_with_pagination() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + // Long waiting period so all relayers stay pending + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, u64::MAX).await; + + // Apply with 3 relayers + for _ in 0..3 { + let (relayer_id, relayer_signer) = create_account(env, 20).await; + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(relayer_id.clone(), relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + } + + // Get all pending relayers + let all: Data> = Contract(contract_id.clone()) + .call_function("get_pending_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(all.data.len(), 3, "Should have 3 pending relayers"); + + // Paginate: first 1 + let page1: Data> = Contract(contract_id.clone()) + .call_function( + "get_pending_relayers", + json!({ "from_index": 0, "limit": 1 }), + ) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(page1.data.len(), 1, "First page should have 1 relayer"); + + // Paginate: skip 1, take 2 + let page2: Data> = Contract(contract_id) + .call_function( + "get_pending_relayers", + json!({ "from_index": 1, "limit": 2 }), + ) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(page2.data.len(), 2, "Second page should have 2 relayers"); +} + +#[tokio::test] +async fn test_get_relayers_mixed_active_and_pending() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + + // First relayer: instantly active (waiting_period = 0) + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, 0).await; + let (active_relayer_id, active_relayer_signer) = create_account(env, 20).await; + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(active_relayer_id.clone(), active_relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + + // Second relayer: pending (long waiting period) + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, u64::MAX).await; + let (pending_relayer_id, pending_relayer_signer) = create_account(env, 20).await; + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(pending_relayer_id.clone(), pending_relayer_signer) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + + // Verify active list + let active: Data> = Contract(contract_id.clone()) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(active.data.len(), 1, "Should have exactly 1 active relayer"); + assert_eq!( + active.data[0].0, + active_relayer_id.to_string(), + "Active relayer should be the one with 0 waiting period" + ); + + // Verify pending list + let pending: Data> = Contract(contract_id) + .call_function("get_pending_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + + assert_eq!(pending.data.len(), 1, "Should have exactly 1 pending relayer"); + assert_eq!( + pending.data[0].0, + pending_relayer_id.to_string(), + "Pending relayer should be the one with long waiting period" + ); +} + +#[tokio::test] +async fn test_get_active_relayers_after_resign() { + let env = get_env().await; + let (contract_id, contract_signer) = deploy_contract(env).await; + let (admin_id, admin_signer) = create_account(env, 10).await; + let (relayer_id, relayer_signer) = create_account(env, 20).await; + + grant_role(env, &contract_id, &contract_signer, "Admin", &admin_id).await; + set_short_config(env, &contract_id, &admin_id, &admin_signer, 5, 0).await; + + // Apply + Contract(contract_id.clone()) + .call_function("apply_for_trusted_relayer", json!({})) + .transaction() + .deposit(near_api::NearToken::from_near(5)) + .with_signer(relayer_id.clone(), relayer_signer.clone()) + .send_to(&env.network) + .await + .expect("Apply should succeed") + .assert_success(); + + // Verify active + let active: Data> = Contract(contract_id.clone()) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + assert_eq!(active.data.len(), 1, "Should have 1 active relayer before resign"); + + // Resign + Contract(contract_id.clone()) + .call_function("resign_trusted_relayer", json!({})) + .transaction() + .with_signer(relayer_id, relayer_signer) + .send_to(&env.network) + .await + .expect("Resign should succeed") + .assert_success(); + + // Verify empty after resign + let active: Data> = Contract(contract_id) + .call_function("get_active_relayers", json!({})) + .read_only() + .fetch_from(&env.network) + .await + .unwrap(); + assert!(active.data.is_empty(), "Should have no active relayers after resign"); +} diff --git a/tests/test-contract-custom/src/lib.rs b/tests/test-contract-custom/src/lib.rs index bf97b73..3cd6a69 100644 --- a/tests/test-contract-custom/src/lib.rs +++ b/tests/test-contract-custom/src/lib.rs @@ -35,7 +35,7 @@ impl TrustedRelayer for CustomContract { } // Fall back to the standard staking map check - omni_utils::trusted_relayer::tr_relayers_map() + omni_utils::trusted_relayer::tr_load_relayers() .get(account_id) .is_some_and(|state| env::block_timestamp() >= state.activate_at.0) } From 143a2761e2e2e342a3cd6fa38ccff295aff8f4eb Mon Sep 17 00:00:00 2001 From: Ivan Frolov Date: Thu, 26 Mar 2026 21:10:52 -0400 Subject: [PATCH 2/5] chore: removed explicit flush --- omni-utils/src/trusted_relayer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/omni-utils/src/trusted_relayer.rs b/omni-utils/src/trusted_relayer.rs index 151fa16..ec74f2e 100644 --- a/omni-utils/src/trusted_relayer.rs +++ b/omni-utils/src/trusted_relayer.rs @@ -84,8 +84,7 @@ pub fn tr_load_relayers() -> IterableMap { /// Flush pending writes and persist the relayers map metadata (length + prefixes) /// so that future calls can restore iteration state. -pub fn tr_save_relayers(mut map: IterableMap) { - map.flush(); +pub fn tr_save_relayers(map: IterableMap) { env::storage_write( TR_RELAYERS_META_KEY, &borsh::to_vec(&map) From d1e4e68be16c58d01d1cd1d8a55f20a2d34ef1b9 Mon Sep 17 00:00:00 2001 From: Ivan Frolov Date: Thu, 26 Mar 2026 21:21:02 -0400 Subject: [PATCH 3/5] refactor: custom wrapper with custom drop to save in state --- omni-utils/src/trusted_relayer.rs | 67 ++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/omni-utils/src/trusted_relayer.rs b/omni-utils/src/trusted_relayer.rs index ec74f2e..4b2d3bd 100644 --- a/omni-utils/src/trusted_relayer.rs +++ b/omni-utils/src/trusted_relayer.rs @@ -1,3 +1,5 @@ +use core::ops::{Deref, DerefMut}; + use near_sdk::borsh::{self, BorshDeserialize}; use near_sdk::json_types::{U64, U128}; use near_sdk::serde_json::json; @@ -72,26 +74,61 @@ pub fn tr_save_config(config: &RelayerConfig) { ); } +/// Wrapper around `IterableMap` that automatically persists iteration metadata +/// (the internal `len` field) on drop. Entry data is flushed by the inner +/// `IterableMap`'s own `Drop` impl; this wrapper handles the metadata that +/// `IterableMap` cannot persist on its own in a detached (non-struct-field) pattern. +/// +/// Metadata is only written when the map has been mutated (via `DerefMut`), +/// so read-only usage in view calls does not trigger `storage_write`. +pub struct RelayerMap { + inner: IterableMap, + modified: bool, +} + +impl Deref for RelayerMap { + type Target = IterableMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for RelayerMap { + fn deref_mut(&mut self) -> &mut Self::Target { + self.modified = true; + &mut self.inner + } +} + +impl Drop for RelayerMap { + fn drop(&mut self) { + if self.modified { + env::storage_write( + TR_RELAYERS_META_KEY, + &borsh::to_vec(&self.inner).unwrap_or_else(|_| { + env::panic_str("Failed to serialize relayers map metadata") + }), + ); + } + } +} + /// Load the relayers map, restoring iteration metadata from storage. /// On first use (no metadata yet), creates a fresh empty map. -pub fn tr_load_relayers() -> IterableMap { - match env::storage_read(TR_RELAYERS_META_KEY) { +/// Metadata is persisted automatically when the returned `RelayerMap` is dropped. +pub fn tr_load_relayers() -> RelayerMap { + let inner = match env::storage_read(TR_RELAYERS_META_KEY) { Some(bytes) => BorshDeserialize::try_from_slice(&bytes) .unwrap_or_else(|_| env::panic_str("Failed to deserialize relayers map metadata")), None => IterableMap::new(TR_RELAYERS_PREFIX), + }; + RelayerMap { + inner, + modified: false, } } -/// Flush pending writes and persist the relayers map metadata (length + prefixes) -/// so that future calls can restore iteration state. -pub fn tr_save_relayers(map: IterableMap) { - env::storage_write( - TR_RELAYERS_META_KEY, - &borsh::to_vec(&map) - .unwrap_or_else(|_| env::panic_str("Failed to serialize relayers map metadata")), - ); -} - /// Trusted relayer staking support for NEAR contracts. /// /// Override `is_trusted_relayer` to add custom bypass logic (e.g. ACL roles). @@ -140,8 +177,6 @@ pub trait TrustedRelayer { }, ); - tr_save_relayers(relayers); - TrustedRelayerEvent::RelayerApplyEvent { account_id: account_id.clone(), stake: config.stake_required, @@ -167,8 +202,6 @@ pub trait TrustedRelayer { "Relayer is not active yet" ); - tr_save_relayers(relayers); - TrustedRelayerEvent::RelayerResignEvent { account_id: account_id.clone(), stake: state.stake, @@ -185,8 +218,6 @@ pub trait TrustedRelayer { .remove(&account_id) .unwrap_or_else(|| env::panic_str("Relayer application not found")); - tr_save_relayers(relayers); - TrustedRelayerEvent::RelayerRejectEvent { account_id: account_id.clone(), stake: state.stake, From 3a1a4840afde13109fb17a0174626152f0e6e721 Mon Sep 17 00:00:00 2001 From: Ivan Frolov Date: Thu, 26 Mar 2026 21:24:01 -0400 Subject: [PATCH 4/5] chore: documented default page limit --- omni-utils/src/trusted_relayer.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/omni-utils/src/trusted_relayer.rs b/omni-utils/src/trusted_relayer.rs index 4b2d3bd..8124f7d 100644 --- a/omni-utils/src/trusted_relayer.rs +++ b/omni-utils/src/trusted_relayer.rs @@ -9,6 +9,7 @@ use near_sdk::{AccountId, NearToken, Promise, env, near, require}; const TR_CONFIG_KEY: &[u8] = b"__tr_config"; const TR_RELAYERS_PREFIX: &[u8] = b"__tr_relayers"; const TR_RELAYERS_META_KEY: &[u8] = b"__tr_relayers_meta"; +const TR_DEFAULT_PAGE_LIMIT: u32 = 100; #[derive(Debug, Clone)] #[near(serializers = [json, borsh])] @@ -263,7 +264,7 @@ pub trait TrustedRelayer { .iter() .filter(|(_, state)| now >= state.activate_at.0) .skip(from_index.unwrap_or(0) as usize) - .take(limit.unwrap_or(100) as usize) + .take(limit.unwrap_or(TR_DEFAULT_PAGE_LIMIT) as usize) .map(|(id, state)| (id.clone(), state.clone())) .collect() } @@ -279,7 +280,7 @@ pub trait TrustedRelayer { .iter() .filter(|(_, state)| now < state.activate_at.0) .skip(from_index.unwrap_or(0) as usize) - .take(limit.unwrap_or(100) as usize) + .take(limit.unwrap_or(TR_DEFAULT_PAGE_LIMIT) as usize) .map(|(id, state)| (id.clone(), state.clone())) .collect() } From e1c7b785960f78455b7f3e519f61d0c98290fbc8 Mon Sep 17 00:00:00 2001 From: Ivan Frolov Date: Thu, 26 Mar 2026 21:24:13 -0400 Subject: [PATCH 5/5] chore: fmt --- omni-utils/tests/trusted_relayer.rs | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/omni-utils/tests/trusted_relayer.rs b/omni-utils/tests/trusted_relayer.rs index 87051bb..eaa6dac 100644 --- a/omni-utils/tests/trusted_relayer.rs +++ b/omni-utils/tests/trusted_relayer.rs @@ -1401,7 +1401,10 @@ async fn test_get_active_relayers_empty() { .await .unwrap(); - assert!(result.data.is_empty(), "Should return empty list when no relayers exist"); + assert!( + result.data.is_empty(), + "Should return empty list when no relayers exist" + ); } #[tokio::test] @@ -1416,7 +1419,10 @@ async fn test_get_pending_relayers_empty() { .await .unwrap(); - assert!(result.data.is_empty(), "Should return empty list when no relayers exist"); + assert!( + result.data.is_empty(), + "Should return empty list when no relayers exist" + ); } #[tokio::test] @@ -1464,7 +1470,10 @@ async fn test_get_pending_relayers_returns_pending() { .await .unwrap(); - assert!(active.data.is_empty(), "Should have no active relayers while pending"); + assert!( + active.data.is_empty(), + "Should have no active relayers while pending" + ); } #[tokio::test] @@ -1512,7 +1521,10 @@ async fn test_get_active_relayers_returns_active() { .await .unwrap(); - assert!(pending.data.is_empty(), "Should have no pending relayers when instantly active"); + assert!( + pending.data.is_empty(), + "Should have no pending relayers when instantly active" + ); } #[tokio::test] @@ -1709,7 +1721,11 @@ async fn test_get_relayers_mixed_active_and_pending() { .await .unwrap(); - assert_eq!(pending.data.len(), 1, "Should have exactly 1 pending relayer"); + assert_eq!( + pending.data.len(), + 1, + "Should have exactly 1 pending relayer" + ); assert_eq!( pending.data[0].0, pending_relayer_id.to_string(), @@ -1745,7 +1761,11 @@ async fn test_get_active_relayers_after_resign() { .fetch_from(&env.network) .await .unwrap(); - assert_eq!(active.data.len(), 1, "Should have 1 active relayer before resign"); + assert_eq!( + active.data.len(), + 1, + "Should have 1 active relayer before resign" + ); // Resign Contract(contract_id.clone()) @@ -1764,5 +1784,8 @@ async fn test_get_active_relayers_after_resign() { .fetch_from(&env.network) .await .unwrap(); - assert!(active.data.is_empty(), "Should have no active relayers after resign"); + assert!( + active.data.is_empty(), + "Should have no active relayers after resign" + ); }