t402 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {t402-1.3.0 → t402-1.4.0}/PKG-INFO +1 -1
  2. {t402-1.3.0 → t402-1.4.0}/pyproject.toml +1 -1
  3. {t402-1.3.0 → t402-1.4.0}/src/t402/__init__.py +46 -0
  4. t402-1.4.0/src/t402/bridge/__init__.py +140 -0
  5. t402-1.4.0/src/t402/bridge/client.py +328 -0
  6. t402-1.4.0/src/t402/bridge/constants.py +275 -0
  7. t402-1.4.0/src/t402/bridge/router.py +221 -0
  8. t402-1.4.0/src/t402/bridge/scan.py +226 -0
  9. t402-1.4.0/src/t402/bridge/types.py +307 -0
  10. {t402-1.3.0 → t402-1.4.0}/.gitignore +0 -0
  11. {t402-1.3.0 → t402-1.4.0}/.python-version +0 -0
  12. {t402-1.3.0 → t402-1.4.0}/README.md +0 -0
  13. {t402-1.3.0 → t402-1.4.0}/src/t402/chains.py +0 -0
  14. {t402-1.3.0 → t402-1.4.0}/src/t402/clients/__init__.py +0 -0
  15. {t402-1.3.0 → t402-1.4.0}/src/t402/clients/base.py +0 -0
  16. {t402-1.3.0 → t402-1.4.0}/src/t402/clients/httpx.py +0 -0
  17. {t402-1.3.0 → t402-1.4.0}/src/t402/clients/requests.py +0 -0
  18. {t402-1.3.0 → t402-1.4.0}/src/t402/common.py +0 -0
  19. {t402-1.3.0 → t402-1.4.0}/src/t402/encoding.py +0 -0
  20. {t402-1.3.0 → t402-1.4.0}/src/t402/erc4337/__init__.py +0 -0
  21. {t402-1.3.0 → t402-1.4.0}/src/t402/erc4337/accounts.py +0 -0
  22. {t402-1.3.0 → t402-1.4.0}/src/t402/erc4337/bundlers.py +0 -0
  23. {t402-1.3.0 → t402-1.4.0}/src/t402/erc4337/paymasters.py +0 -0
  24. {t402-1.3.0 → t402-1.4.0}/src/t402/erc4337/types.py +0 -0
  25. {t402-1.3.0 → t402-1.4.0}/src/t402/evm_paywall_template.py +0 -0
  26. {t402-1.3.0 → t402-1.4.0}/src/t402/exact.py +0 -0
  27. {t402-1.3.0 → t402-1.4.0}/src/t402/facilitator.py +0 -0
  28. {t402-1.3.0 → t402-1.4.0}/src/t402/fastapi/__init__.py +0 -0
  29. {t402-1.3.0 → t402-1.4.0}/src/t402/fastapi/middleware.py +0 -0
  30. {t402-1.3.0 → t402-1.4.0}/src/t402/flask/__init__.py +0 -0
  31. {t402-1.3.0 → t402-1.4.0}/src/t402/flask/middleware.py +0 -0
  32. {t402-1.3.0 → t402-1.4.0}/src/t402/networks.py +0 -0
  33. {t402-1.3.0 → t402-1.4.0}/src/t402/path.py +0 -0
  34. {t402-1.3.0 → t402-1.4.0}/src/t402/paywall.py +0 -0
  35. {t402-1.3.0 → t402-1.4.0}/src/t402/py.typed +0 -0
  36. {t402-1.3.0 → t402-1.4.0}/src/t402/svm_paywall_template.py +0 -0
  37. {t402-1.3.0 → t402-1.4.0}/src/t402/ton.py +0 -0
  38. {t402-1.3.0 → t402-1.4.0}/src/t402/ton_paywall_template.py +0 -0
  39. {t402-1.3.0 → t402-1.4.0}/src/t402/tron.py +0 -0
  40. {t402-1.3.0 → t402-1.4.0}/src/t402/types.py +0 -0
  41. {t402-1.3.0 → t402-1.4.0}/tests/clients/__init__.py +0 -0
  42. {t402-1.3.0 → t402-1.4.0}/tests/clients/test_base.py +0 -0
  43. {t402-1.3.0 → t402-1.4.0}/tests/clients/test_httpx.py +0 -0
  44. {t402-1.3.0 → t402-1.4.0}/tests/clients/test_requests.py +0 -0
  45. {t402-1.3.0 → t402-1.4.0}/tests/fastapi_tests/__init__.py +0 -0
  46. {t402-1.3.0 → t402-1.4.0}/tests/fastapi_tests/test_middleware.py +0 -0
  47. {t402-1.3.0 → t402-1.4.0}/tests/flask_tests/__init__.py +0 -0
  48. {t402-1.3.0 → t402-1.4.0}/tests/flask_tests/test_middleware.py +0 -0
  49. {t402-1.3.0 → t402-1.4.0}/tests/test_common.py +0 -0
  50. {t402-1.3.0 → t402-1.4.0}/tests/test_encoding.py +0 -0
  51. {t402-1.3.0 → t402-1.4.0}/tests/test_exact.py +0 -0
  52. {t402-1.3.0 → t402-1.4.0}/tests/test_paywall.py +0 -0
  53. {t402-1.3.0 → t402-1.4.0}/tests/test_ton.py +0 -0
  54. {t402-1.3.0 → t402-1.4.0}/tests/test_tron.py +0 -0
  55. {t402-1.3.0 → t402-1.4.0}/tests/test_types.py +0 -0
  56. {t402-1.3.0 → t402-1.4.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: t402
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: t402: An internet native payments protocol
5
5
  Author-email: T402 Team <dev@t402.io>
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "t402"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "t402: An internet native payments protocol"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
@@ -91,6 +91,32 @@ from t402.erc4337 import (
91
91
  SafeAccountConfig,
92
92
  create_smart_account,
93
93
  )
94
+ from t402.bridge import (
95
+ # Client
96
+ Usdt0Bridge,
97
+ create_usdt0_bridge,
98
+ # LayerZero Scan
99
+ LayerZeroScanClient,
100
+ create_layerzero_scan_client,
101
+ # Router
102
+ CrossChainPaymentRouter,
103
+ create_cross_chain_payment_router,
104
+ # Constants
105
+ LAYERZERO_ENDPOINT_IDS,
106
+ USDT0_OFT_ADDRESSES,
107
+ LAYERZERO_SCAN_BASE_URL,
108
+ get_bridgeable_chains,
109
+ supports_bridging,
110
+ # Types
111
+ BridgeQuoteParams,
112
+ BridgeQuote,
113
+ BridgeExecuteParams,
114
+ BridgeResult,
115
+ LayerZeroMessage,
116
+ LayerZeroMessageStatus,
117
+ CrossChainPaymentParams,
118
+ CrossChainPaymentResult,
119
+ )
94
120
 
95
121
  def hello() -> str:
96
122
  return "Hello from t402!"
@@ -180,4 +206,24 @@ __all__ = [
180
206
  "SafeSmartAccount",
181
207
  "SafeAccountConfig",
182
208
  "create_smart_account",
209
+ # USDT0 Bridge
210
+ "Usdt0Bridge",
211
+ "create_usdt0_bridge",
212
+ "LayerZeroScanClient",
213
+ "create_layerzero_scan_client",
214
+ "CrossChainPaymentRouter",
215
+ "create_cross_chain_payment_router",
216
+ "LAYERZERO_ENDPOINT_IDS",
217
+ "USDT0_OFT_ADDRESSES",
218
+ "LAYERZERO_SCAN_BASE_URL",
219
+ "get_bridgeable_chains",
220
+ "supports_bridging",
221
+ "BridgeQuoteParams",
222
+ "BridgeQuote",
223
+ "BridgeExecuteParams",
224
+ "BridgeResult",
225
+ "LayerZeroMessage",
226
+ "LayerZeroMessageStatus",
227
+ "CrossChainPaymentParams",
228
+ "CrossChainPaymentResult",
183
229
  ]
@@ -0,0 +1,140 @@
1
+ """USDT0 Cross-Chain Bridge Module.
2
+
3
+ Provides cross-chain USDT0 transfers using LayerZero OFT standard,
4
+ with message tracking via LayerZero Scan API.
5
+
6
+ Example:
7
+ ```python
8
+ from t402.bridge import (
9
+ Usdt0Bridge,
10
+ LayerZeroScanClient,
11
+ CrossChainPaymentRouter,
12
+ get_bridgeable_chains,
13
+ )
14
+
15
+ # Check supported chains
16
+ print(get_bridgeable_chains()) # ['ethereum', 'arbitrum', 'ink', ...]
17
+
18
+ # Create bridge client
19
+ bridge = Usdt0Bridge(signer, 'arbitrum')
20
+
21
+ # Get quote
22
+ quote = await bridge.quote(BridgeQuoteParams(
23
+ from_chain='arbitrum',
24
+ to_chain='ethereum',
25
+ amount=100_000000,
26
+ recipient='0x...',
27
+ ))
28
+
29
+ print(f"Fee: {quote.native_fee} wei")
30
+
31
+ # Execute bridge
32
+ result = await bridge.send(BridgeExecuteParams(
33
+ from_chain='arbitrum',
34
+ to_chain='ethereum',
35
+ amount=100_000000,
36
+ recipient='0x...',
37
+ ))
38
+
39
+ print(f"Bridge tx: {result.tx_hash}")
40
+ print(f"Message GUID: {result.message_guid}")
41
+
42
+ # Track message delivery
43
+ scan_client = LayerZeroScanClient()
44
+ message = await scan_client.wait_for_delivery(
45
+ result.message_guid,
46
+ on_status_change=lambda s: print(f"Status: {s}")
47
+ )
48
+ print(f"Delivered! Dest TX: {message.dst_tx_hash}")
49
+ ```
50
+ """
51
+
52
+ # Bridge client
53
+ from .client import Usdt0Bridge, create_usdt0_bridge
54
+
55
+ # LayerZero Scan client
56
+ from .scan import LayerZeroScanClient, create_layerzero_scan_client
57
+
58
+ # Cross-chain payment router
59
+ from .router import CrossChainPaymentRouter, create_cross_chain_payment_router
60
+
61
+ # Constants
62
+ from .constants import (
63
+ LAYERZERO_ENDPOINT_IDS,
64
+ USDT0_OFT_ADDRESSES,
65
+ LAYERZERO_SCAN_BASE_URL,
66
+ NETWORK_TO_CHAIN,
67
+ CHAIN_TO_NETWORK,
68
+ get_endpoint_id,
69
+ get_endpoint_id_from_network,
70
+ get_usdt0_oft_address,
71
+ supports_bridging,
72
+ get_bridgeable_chains,
73
+ address_to_bytes32,
74
+ bytes32_to_address,
75
+ )
76
+
77
+ # Types
78
+ from .types import (
79
+ # Bridge types
80
+ BridgeQuoteParams,
81
+ BridgeQuote,
82
+ BridgeExecuteParams,
83
+ BridgeResult,
84
+ BridgeStatus,
85
+ BridgeSigner,
86
+ SendParam,
87
+ MessagingFee,
88
+ TransactionLog,
89
+ BridgeTransactionReceipt,
90
+ # LayerZero Scan types
91
+ LayerZeroMessage,
92
+ LayerZeroMessageStatus,
93
+ WaitForDeliveryOptions,
94
+ # Cross-chain payment types
95
+ CrossChainPaymentParams,
96
+ CrossChainPaymentResult,
97
+ )
98
+
99
+ __all__ = [
100
+ # Bridge client
101
+ "Usdt0Bridge",
102
+ "create_usdt0_bridge",
103
+ # LayerZero Scan client
104
+ "LayerZeroScanClient",
105
+ "create_layerzero_scan_client",
106
+ # Cross-chain payment router
107
+ "CrossChainPaymentRouter",
108
+ "create_cross_chain_payment_router",
109
+ # Constants
110
+ "LAYERZERO_ENDPOINT_IDS",
111
+ "USDT0_OFT_ADDRESSES",
112
+ "LAYERZERO_SCAN_BASE_URL",
113
+ "NETWORK_TO_CHAIN",
114
+ "CHAIN_TO_NETWORK",
115
+ "get_endpoint_id",
116
+ "get_endpoint_id_from_network",
117
+ "get_usdt0_oft_address",
118
+ "supports_bridging",
119
+ "get_bridgeable_chains",
120
+ "address_to_bytes32",
121
+ "bytes32_to_address",
122
+ # Bridge types
123
+ "BridgeQuoteParams",
124
+ "BridgeQuote",
125
+ "BridgeExecuteParams",
126
+ "BridgeResult",
127
+ "BridgeStatus",
128
+ "BridgeSigner",
129
+ "SendParam",
130
+ "MessagingFee",
131
+ "TransactionLog",
132
+ "BridgeTransactionReceipt",
133
+ # LayerZero Scan types
134
+ "LayerZeroMessage",
135
+ "LayerZeroMessageStatus",
136
+ "WaitForDeliveryOptions",
137
+ # Cross-chain payment types
138
+ "CrossChainPaymentParams",
139
+ "CrossChainPaymentResult",
140
+ ]
@@ -0,0 +1,328 @@
1
+ """USDT0 Bridge Client for LayerZero OFT transfers."""
2
+
3
+ from typing import Optional
4
+
5
+ from .constants import (
6
+ DEFAULT_EXTRA_OPTIONS,
7
+ DEFAULT_SLIPPAGE,
8
+ ERC20_APPROVE_ABI,
9
+ ESTIMATED_BRIDGE_TIME,
10
+ OFT_SEND_ABI,
11
+ OFT_SENT_EVENT_TOPIC,
12
+ address_to_bytes32,
13
+ get_bridgeable_chains,
14
+ get_endpoint_id,
15
+ get_usdt0_oft_address,
16
+ supports_bridging,
17
+ )
18
+ from .types import (
19
+ BridgeExecuteParams,
20
+ BridgeQuote,
21
+ BridgeQuoteParams,
22
+ BridgeResult,
23
+ BridgeSigner,
24
+ BridgeTransactionReceipt,
25
+ MessagingFee,
26
+ SendParam,
27
+ )
28
+
29
+
30
+ class Usdt0Bridge:
31
+ """USDT0 Bridge Client for LayerZero OFT transfers.
32
+
33
+ Provides cross-chain USDT0 transfers using LayerZero OFT standard.
34
+
35
+ Example:
36
+ ```python
37
+ from t402.bridge import Usdt0Bridge
38
+
39
+ bridge = Usdt0Bridge(signer, 'arbitrum')
40
+
41
+ # Get quote
42
+ quote = await bridge.quote(BridgeQuoteParams(
43
+ from_chain='arbitrum',
44
+ to_chain='ethereum',
45
+ amount=100_000000, # 100 USDT0
46
+ recipient='0x...',
47
+ ))
48
+
49
+ print(f"Fee: {quote.native_fee} wei")
50
+
51
+ # Execute bridge
52
+ result = await bridge.send(BridgeExecuteParams(
53
+ from_chain='arbitrum',
54
+ to_chain='ethereum',
55
+ amount=100_000000,
56
+ recipient='0x...',
57
+ ))
58
+
59
+ print(f"Bridge tx: {result.tx_hash}")
60
+ print(f"Message GUID: {result.message_guid}")
61
+ ```
62
+ """
63
+
64
+ def __init__(self, signer: BridgeSigner, chain: str) -> None:
65
+ """Create a new bridge client.
66
+
67
+ Args:
68
+ signer: Wallet signer with read/write capabilities
69
+ chain: Source chain name (e.g., 'arbitrum', 'ethereum')
70
+
71
+ Raises:
72
+ ValueError: If chain doesn't support bridging
73
+ """
74
+ if not supports_bridging(chain):
75
+ raise ValueError(
76
+ f'Chain "{chain}" does not support USDT0 bridging. '
77
+ f'Supported chains: {", ".join(get_bridgeable_chains())}'
78
+ )
79
+
80
+ self._signer = signer
81
+ self._chain = chain.lower()
82
+
83
+ async def quote(self, params: BridgeQuoteParams) -> BridgeQuote:
84
+ """Get a quote for bridging USDT0.
85
+
86
+ Args:
87
+ params: Bridge parameters
88
+
89
+ Returns:
90
+ Quote with fee and amount information
91
+
92
+ Raises:
93
+ ValueError: If parameters are invalid
94
+ """
95
+ self._validate_params(params)
96
+
97
+ send_param = self._build_send_param(
98
+ params.to_chain, params.amount, params.recipient, DEFAULT_SLIPPAGE
99
+ )
100
+ oft_address = get_usdt0_oft_address(params.from_chain)
101
+
102
+ # Get quote from contract
103
+ fee = await self._signer.read_contract(
104
+ oft_address,
105
+ OFT_SEND_ABI,
106
+ "quoteSend",
107
+ self._send_param_to_tuple(send_param),
108
+ False,
109
+ )
110
+
111
+ native_fee = fee[0] if isinstance(fee, (list, tuple)) else fee.get("nativeFee", 0)
112
+
113
+ return BridgeQuote(
114
+ native_fee=int(native_fee),
115
+ amount_to_send=params.amount,
116
+ min_amount_to_receive=send_param.min_amount_ld,
117
+ estimated_time=ESTIMATED_BRIDGE_TIME,
118
+ from_chain=params.from_chain,
119
+ to_chain=params.to_chain,
120
+ )
121
+
122
+ async def send(self, params: BridgeExecuteParams) -> BridgeResult:
123
+ """Execute a bridge transaction.
124
+
125
+ Args:
126
+ params: Bridge execution parameters
127
+
128
+ Returns:
129
+ Bridge result with transaction hash and message GUID
130
+
131
+ Raises:
132
+ ValueError: If parameters are invalid or transaction fails
133
+ """
134
+ self._validate_params(
135
+ BridgeQuoteParams(
136
+ from_chain=params.from_chain,
137
+ to_chain=params.to_chain,
138
+ amount=params.amount,
139
+ recipient=params.recipient,
140
+ )
141
+ )
142
+
143
+ slippage = params.slippage_tolerance if params.slippage_tolerance > 0 else DEFAULT_SLIPPAGE
144
+ oft_address = get_usdt0_oft_address(params.from_chain)
145
+ send_param = self._build_send_param(
146
+ params.to_chain, params.amount, params.recipient, slippage
147
+ )
148
+ refund_address = params.refund_address or self._signer.address
149
+
150
+ # Get fee quote
151
+ fee_result = await self._signer.read_contract(
152
+ oft_address,
153
+ OFT_SEND_ABI,
154
+ "quoteSend",
155
+ self._send_param_to_tuple(send_param),
156
+ False,
157
+ )
158
+
159
+ if isinstance(fee_result, (list, tuple)):
160
+ native_fee = int(fee_result[0])
161
+ lz_token_fee = int(fee_result[1]) if len(fee_result) > 1 else 0
162
+ else:
163
+ native_fee = int(fee_result.get("nativeFee", 0))
164
+ lz_token_fee = int(fee_result.get("lzTokenFee", 0))
165
+
166
+ fee = MessagingFee(native_fee=native_fee, lz_token_fee=lz_token_fee)
167
+
168
+ # Check and approve allowance if needed
169
+ await self._ensure_allowance(oft_address, params.amount)
170
+
171
+ # Execute bridge transaction
172
+ tx_hash = await self._signer.write_contract(
173
+ oft_address,
174
+ OFT_SEND_ABI,
175
+ "send",
176
+ self._send_param_to_tuple(send_param),
177
+ (fee.native_fee, fee.lz_token_fee),
178
+ refund_address,
179
+ value=fee.native_fee,
180
+ )
181
+
182
+ # Wait for transaction confirmation
183
+ receipt = await self._signer.wait_for_transaction_receipt(tx_hash)
184
+
185
+ if receipt.status != 1:
186
+ raise ValueError(f"Bridge transaction failed: {tx_hash}")
187
+
188
+ # Extract message GUID from OFTSent event logs
189
+ message_guid = self._extract_message_guid(receipt)
190
+
191
+ return BridgeResult(
192
+ tx_hash=tx_hash,
193
+ message_guid=message_guid,
194
+ amount_sent=params.amount,
195
+ amount_to_receive=send_param.min_amount_ld,
196
+ from_chain=params.from_chain,
197
+ to_chain=params.to_chain,
198
+ estimated_time=ESTIMATED_BRIDGE_TIME,
199
+ )
200
+
201
+ def get_supported_destinations(self) -> list[str]:
202
+ """Get all supported destination chains from current chain.
203
+
204
+ Returns:
205
+ List of supported destination chain names
206
+ """
207
+ return [c for c in get_bridgeable_chains() if c != self._chain]
208
+
209
+ def supports_destination(self, to_chain: str) -> bool:
210
+ """Check if a destination chain is supported.
211
+
212
+ Args:
213
+ to_chain: Destination chain name
214
+
215
+ Returns:
216
+ True if supported
217
+ """
218
+ return to_chain.lower() != self._chain and supports_bridging(to_chain)
219
+
220
+ def _validate_params(self, params: BridgeQuoteParams) -> None:
221
+ """Validate bridge parameters."""
222
+ if params.from_chain.lower() != self._chain:
223
+ raise ValueError(
224
+ f'Source chain mismatch: bridge initialized for "{self._chain}" '
225
+ f'but got "{params.from_chain}"'
226
+ )
227
+
228
+ if not supports_bridging(params.from_chain):
229
+ raise ValueError(
230
+ f'Source chain "{params.from_chain}" does not support USDT0 bridging'
231
+ )
232
+
233
+ if not supports_bridging(params.to_chain):
234
+ raise ValueError(
235
+ f'Destination chain "{params.to_chain}" does not support USDT0 bridging'
236
+ )
237
+
238
+ if params.from_chain.lower() == params.to_chain.lower():
239
+ raise ValueError("Source and destination chains must be different")
240
+
241
+ if params.amount <= 0:
242
+ raise ValueError("Amount must be greater than 0")
243
+
244
+ def _build_send_param(
245
+ self, to_chain: str, amount: int, recipient: str, slippage: float
246
+ ) -> SendParam:
247
+ """Build the LayerZero SendParam struct."""
248
+ dst_eid = get_endpoint_id(to_chain)
249
+ if dst_eid is None:
250
+ raise ValueError(f"Unknown destination chain: {to_chain}")
251
+
252
+ to_bytes = address_to_bytes32(recipient)
253
+
254
+ # Calculate minimum amount with slippage
255
+ slippage_bps = int(slippage * 100)
256
+ min_amount = amount - (amount * slippage_bps) // 10000
257
+
258
+ return SendParam(
259
+ dst_eid=dst_eid,
260
+ to=to_bytes,
261
+ amount_ld=amount,
262
+ min_amount_ld=min_amount,
263
+ extra_options=DEFAULT_EXTRA_OPTIONS,
264
+ compose_msg=b"",
265
+ oft_cmd=b"",
266
+ )
267
+
268
+ def _send_param_to_tuple(self, param: SendParam) -> tuple:
269
+ """Convert SendParam to tuple for contract call."""
270
+ return (
271
+ param.dst_eid,
272
+ param.to,
273
+ param.amount_ld,
274
+ param.min_amount_ld,
275
+ param.extra_options,
276
+ param.compose_msg,
277
+ param.oft_cmd,
278
+ )
279
+
280
+ async def _ensure_allowance(self, oft_address: str, amount: int) -> None:
281
+ """Check and approve token allowance if needed."""
282
+ signer_address = self._signer.address
283
+
284
+ # Check current allowance
285
+ allowance = await self._signer.read_contract(
286
+ oft_address,
287
+ ERC20_APPROVE_ABI,
288
+ "allowance",
289
+ signer_address,
290
+ oft_address,
291
+ )
292
+
293
+ allowance_int = int(allowance) if not isinstance(allowance, int) else allowance
294
+
295
+ # Approve if needed
296
+ if allowance_int < amount:
297
+ await self._signer.write_contract(
298
+ oft_address,
299
+ ERC20_APPROVE_ABI,
300
+ "approve",
301
+ oft_address,
302
+ amount,
303
+ )
304
+
305
+ def _extract_message_guid(self, receipt: BridgeTransactionReceipt) -> str:
306
+ """Extract LayerZero message GUID from OFTSent event logs."""
307
+ for log in receipt.logs:
308
+ if len(log.topics) >= 2 and log.topics[0].lower() == OFT_SENT_EVENT_TOPIC.lower():
309
+ # GUID is the first indexed parameter (topics[1])
310
+ return log.topics[1]
311
+
312
+ raise ValueError(
313
+ "Failed to extract message GUID from transaction logs. "
314
+ "The OFTSent event was not found in the transaction receipt."
315
+ )
316
+
317
+
318
+ def create_usdt0_bridge(signer: BridgeSigner, chain: str) -> Usdt0Bridge:
319
+ """Create a bridge client for a specific chain.
320
+
321
+ Args:
322
+ signer: Wallet signer
323
+ chain: Source chain name
324
+
325
+ Returns:
326
+ New bridge client instance
327
+ """
328
+ return Usdt0Bridge(signer, chain)