Skip to content

Commit ffb0c2f

Browse files
Bridgerzclaude
andauthored
Index V2 bridge events with message_timestamp_ms (#25421)
## Summary Adds V2 bridge event indexing and a new `message_timestamp_ms` column so the bridge frontend can determine limiter-bypass eligibility. ### Problem V2 bridge transfers embed a timestamp in the message payload. Transfers older than 48 hours bypass the rate limiter. The indexer wasn't capturing this timestamp, so the frontend had no way to show bypass eligibility. ### Changes **Database migration** - Adds `message_timestamp_ms BIGINT` (nullable) to `token_transfer_data` - `NULL` for V1 transfers, populated for V2 transfers - Safe metadata-only `ALTER TABLE ADD COLUMN` (no table rewrite, no long locks) **ETH indexer** (`sui-bridge-indexer`) - `TokensDeposited` (V1): sets `message_timestamp_ms = NULL` - `TokensDepositedV2`: extracts `timestampSeconds` from the event, converts to ms **Sui alt indexer** (`sui-bridge-indexer-alt`) - `token_transfer_data_handler`: now handles both `TokenDepositedEvent` (V1) and `TokenDepositedEventV2` - `token_transfer_handler`: now handles `TokenDepositedEventV2` with appropriate metrics **Frontend usage** ``` IF message_timestamp_ms IS NOT NULL AND now() - message_timestamp_ms > 48 hours THEN transfer is limiter-bypass eligible ``` ### Testing - All three crates (`sui-bridge-schema`, `sui-bridge-indexer`, `sui-bridge-indexer-alt`) compile cleanly with no warnings --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7c7e923 commit ffb0c2f

File tree

13 files changed

+99
-25
lines changed

13 files changed

+99
-25
lines changed

Cargo.lock

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sui-bridge-indexer-alt/src/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const COMMITTEE: &IdentStr = ident_str!("committee");
1818
const TREASURY: &IdentStr = ident_str!("treasury");
1919

2020
const TOKEN_DEPOSITED_EVENT: &IdentStr = ident_str!("TokenDepositedEvent");
21+
const TOKEN_DEPOSITED_EVENT_V2: &IdentStr = ident_str!("TokenDepositedEventV2");
2122
const TOKEN_TRANSFER_APPROVED: &IdentStr = ident_str!("TokenTransferApproved");
2223
const TOKEN_TRANSFER_CLAIMED: &IdentStr = ident_str!("TokenTransferClaimed");
2324

crates/sui-bridge-indexer-alt/src/handlers/token_transfer_data_handler.rs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// Copyright (c) Mysten Labs, Inc.
22
// SPDX-License-Identifier: Apache-2.0
3-
use crate::handlers::{BRIDGE, TOKEN_DEPOSITED_EVENT, is_bridge_txn};
3+
use crate::handlers::{BRIDGE, TOKEN_DEPOSITED_EVENT, TOKEN_DEPOSITED_EVENT_V2, is_bridge_txn};
44
use crate::struct_tag;
55
use async_trait::async_trait;
66
use diesel_async::RunQueryDsl;
77
use move_core_types::language_storage::StructTag;
88
use std::sync::Arc;
9-
use sui_bridge::events::MoveTokenDepositedEvent;
9+
use sui_bridge::events::{MoveTokenDepositedEvent, MoveTokenDepositedEventV2};
1010
use sui_bridge_schema::models::TokenTransferData;
1111
use sui_bridge_schema::schema::token_transfer_data;
1212
use sui_indexer_alt_framework::pipeline::Processor;
@@ -18,12 +18,14 @@ use tracing::info;
1818

1919
pub struct TokenTransferDataHandler {
2020
deposited_event_type: StructTag,
21+
deposited_event_v2_type: StructTag,
2122
}
2223

2324
impl Default for TokenTransferDataHandler {
2425
fn default() -> Self {
2526
Self {
2627
deposited_event_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_DEPOSITED_EVENT),
28+
deposited_event_v2_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_DEPOSITED_EVENT_V2),
2729
}
2830
}
2931
}
@@ -47,24 +49,41 @@ impl Processor for TokenTransferDataHandler {
4749
continue;
4850
}
4951
for ev in tx.events.iter().flat_map(|e| &e.data) {
50-
if self.deposited_event_type != ev.type_ {
51-
continue;
52+
if self.deposited_event_type == ev.type_ {
53+
info!(?ev, "Observed Sui Deposit");
54+
let event: MoveTokenDepositedEvent = bcs::from_bytes(&ev.contents)?;
55+
results.push(TokenTransferData {
56+
chain_id: event.source_chain as i32,
57+
nonce: event.seq_num as i64,
58+
block_height,
59+
timestamp_ms,
60+
destination_chain: event.target_chain as i32,
61+
sender_address: event.sender_address.clone(),
62+
recipient_address: event.target_address.clone(),
63+
token_id: event.token_type as i32,
64+
amount: event.amount_sui_adjusted as i64,
65+
is_finalized: true,
66+
txn_hash: tx.transaction.digest().inner().to_vec(),
67+
message_timestamp_ms: None,
68+
});
69+
} else if self.deposited_event_v2_type == ev.type_ {
70+
info!(?ev, "Observed Sui V2 Deposit");
71+
let event: MoveTokenDepositedEventV2 = bcs::from_bytes(&ev.contents)?;
72+
results.push(TokenTransferData {
73+
chain_id: event.source_chain as i32,
74+
nonce: event.seq_num as i64,
75+
block_height,
76+
timestamp_ms,
77+
destination_chain: event.target_chain as i32,
78+
sender_address: event.sender_address.clone(),
79+
recipient_address: event.target_address.clone(),
80+
token_id: event.token_type as i32,
81+
amount: event.amount_sui_adjusted as i64,
82+
is_finalized: true,
83+
txn_hash: tx.transaction.digest().inner().to_vec(),
84+
message_timestamp_ms: Some(event.timestamp_ms as i64),
85+
});
5286
}
53-
info!(?ev, "Observed Sui Deposit");
54-
let event: MoveTokenDepositedEvent = bcs::from_bytes(&ev.contents)?;
55-
results.push(TokenTransferData {
56-
chain_id: event.source_chain as i32,
57-
nonce: event.seq_num as i64,
58-
block_height,
59-
timestamp_ms,
60-
destination_chain: event.target_chain as i32,
61-
sender_address: event.sender_address.clone(),
62-
recipient_address: event.target_address.clone(),
63-
token_id: event.token_type as i32,
64-
amount: event.amount_sui_adjusted as i64,
65-
is_finalized: true,
66-
txn_hash: tx.transaction.digest().inner().to_vec(),
67-
});
6887
}
6988
}
7089
Ok(results)

crates/sui-bridge-indexer-alt/src/handlers/token_transfer_handler.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright (c) Mysten Labs, Inc.
22
// SPDX-License-Identifier: Apache-2.0
33
use crate::handlers::{
4-
BRIDGE, TOKEN_DEPOSITED_EVENT, TOKEN_TRANSFER_APPROVED, TOKEN_TRANSFER_CLAIMED, is_bridge_txn,
4+
BRIDGE, TOKEN_DEPOSITED_EVENT, TOKEN_DEPOSITED_EVENT_V2, TOKEN_TRANSFER_APPROVED,
5+
TOKEN_TRANSFER_CLAIMED, is_bridge_txn,
56
};
67
use crate::metrics::BridgeIndexerMetrics;
78
use crate::struct_tag;
@@ -10,7 +11,8 @@ use diesel_async::RunQueryDsl;
1011
use move_core_types::language_storage::StructTag;
1112
use std::sync::Arc;
1213
use sui_bridge::events::{
13-
MoveTokenDepositedEvent, MoveTokenTransferApproved, MoveTokenTransferClaimed,
14+
MoveTokenDepositedEvent, MoveTokenDepositedEventV2, MoveTokenTransferApproved,
15+
MoveTokenTransferClaimed,
1416
};
1517
use sui_bridge_schema::models::{BridgeDataSource, TokenTransfer, TokenTransferStatus};
1618
use sui_bridge_schema::schema::token_transfer;
@@ -25,6 +27,7 @@ use tracing::info;
2527

2628
pub struct TokenTransferHandler {
2729
deposited_event_type: StructTag,
30+
deposited_event_v2_type: StructTag,
2831
approved_event_type: StructTag,
2932
claimed_event_type: StructTag,
3033
metrics: Arc<BridgeIndexerMetrics>,
@@ -34,6 +37,7 @@ impl TokenTransferHandler {
3437
pub fn new(metrics: Arc<BridgeIndexerMetrics>) -> Self {
3538
Self {
3639
deposited_event_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_DEPOSITED_EVENT),
40+
deposited_event_v2_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_DEPOSITED_EVENT_V2),
3741
approved_event_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_TRANSFER_APPROVED),
3842
claimed_event_type: struct_tag!(BRIDGE_ADDRESS, BRIDGE, TOKEN_TRANSFER_CLAIMED),
3943
metrics,
@@ -92,6 +96,29 @@ impl Processor for TokenTransferHandler {
9296
.with_label_values(&["sui_to_eth", "true"])
9397
.inc_by(tx.effects.gas_cost_summary().net_gas_usage() as u64);
9498

99+
(event.source_chain, event.seq_num)
100+
} else if self.deposited_event_v2_type == ev.type_ {
101+
info!("Observed Sui V2 Deposit {:?}", ev);
102+
let event: MoveTokenDepositedEventV2 = bcs::from_bytes(&ev.contents)?;
103+
104+
// Bridge-specific metrics for V2 token deposits
105+
self.metrics
106+
.bridge_events_total
107+
.with_label_values(&["token_deposited_v2", "sui"])
108+
.inc();
109+
self.metrics
110+
.token_transfers_total
111+
.with_label_values(&[
112+
"sui_to_eth",
113+
"deposited",
114+
&event.token_type.to_string(),
115+
])
116+
.inc();
117+
self.metrics
118+
.token_transfer_gas_used
119+
.with_label_values(&["sui_to_eth", "true"])
120+
.inc_by(tx.effects.gas_cost_summary().net_gas_usage() as u64);
121+
95122
(event.source_chain, event.seq_num)
96123
} else if self.approved_event_type == ev.type_ {
97124
info!("Observed Sui Approval {:?}", ev);

crates/sui-bridge-indexer/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ tracing.workspace = true
3535
backoff.workspace = true
3636
sui-config.workspace = true
3737
tempfile.workspace = true
38+
rustls.workspace = true
3839

3940
[dev-dependencies]
4041
sui-test-transaction-builder.workspace = true

crates/sui-bridge-indexer/src/eth_bridge_indexer.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,16 +530,21 @@ impl DataMapper<RawEthData, ProcessedTxnData> for EthDataMapper {
530530
token_id: bridge_event.tokenID,
531531
amount: bridge_event.suiAdjustedAmount,
532532
is_finalized,
533+
message_timestamp_ms: None,
533534
}),
534535
}));
535536
}
536537
EthSuiBridgeEvents::TokensDepositedV2(bridge_event) => {
537538
info!(
538-
"Observed Eth Deposit at block: {}, tx_hash: {}",
539+
"Observed Eth V2 Deposit at block: {}, tx_hash: {}",
539540
log.block_number(),
540541
log.tx_hash
541542
);
542543
self.metrics.total_eth_token_deposited.inc();
544+
// V2 event carries `timestampSeconds` from the EVM contract.
545+
// Convert seconds → milliseconds so the frontend can compare
546+
// against the 48-hour limiter-bypass window.
547+
let msg_timestamp_ms = bridge_event.timestampSeconds.to::<u64>() * 1000;
543548
processed_txn_data.push(ProcessedTxnData::TokenTransfer(TokenTransfer {
544549
chain_id: bridge_event.sourceChainID,
545550
nonce: bridge_event.nonce,
@@ -558,6 +563,7 @@ impl DataMapper<RawEthData, ProcessedTxnData> for EthDataMapper {
558563
token_id: bridge_event.tokenID,
559564
amount: bridge_event.suiAdjustedAmount,
560565
is_finalized,
566+
message_timestamp_ms: Some(msg_timestamp_ms),
561567
}),
562568
}));
563569
}

crates/sui-bridge-indexer/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ pub struct TokenTransferData {
8282
token_id: u8,
8383
amount: u64,
8484
is_finalized: bool,
85+
/// For V2 transfers, the timestamp (in ms) from the bridge message payload.
86+
/// `None` for V1 transfers.
87+
message_timestamp_ms: Option<u64>,
8588
}
8689

8790
impl TokenTransfer {
@@ -113,6 +116,7 @@ impl TokenTransfer {
113116
token_id: data.token_id as i32,
114117
amount: data.amount as i64,
115118
is_finalized: data.is_finalized,
119+
message_timestamp_ms: data.message_timestamp_ms.map(|ts| ts as i64),
116120
})
117121
}
118122
}

crates/sui-bridge-indexer/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ struct Args {
4545

4646
#[tokio::main]
4747
async fn main() -> Result<()> {
48+
rustls::crypto::ring::default_provider()
49+
.install_default()
50+
.expect("Failed to install CryptoProvider");
51+
4852
let _guard = telemetry_subscribers::TelemetryConfig::new()
4953
.with_env()
5054
.init();

crates/sui-bridge-indexer/src/storage.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ impl Persistent<ProcessedTxnData> for PgBridgePersistent {
133133
.eq(excluded(token_transfer_data::amount)),
134134
token_transfer_data::is_finalized
135135
.eq(excluded(token_transfer_data::is_finalized)),
136+
token_transfer_data::message_timestamp_ms.eq(excluded(
137+
token_transfer_data::message_timestamp_ms,
138+
)),
136139
))
137140
.filter(token_transfer_data::is_finalized.eq(false))
138141
.execute(conn)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE token_transfer_data DROP COLUMN message_timestamp_ms;

0 commit comments

Comments
 (0)