feat(tron): add HD wallet activation via EthCoin pipeline#2467
feat(tron): add HD wallet activation via EthCoin pipeline#2467
Conversation
onur-ozkan
left a comment
There was a problem hiding this comment.
What I realized while reviewing this PR is that we are adding a lot of TODOs to the codebase. It's probably much better to create a tracking issue for them instead.
mariocynicys
left a comment
There was a problem hiding this comment.
Thanks!
first iteration
The function is already gated with #[cfg(not(target_arch = "wasm32"))], so the inner cfg only needs #[cfg(test)]. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace get_passphrase(.env.client) with Mm2TestConfForSwap::BOB_HD_PASSPHRASE for basic TRON activation tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use address_id: 0 instead of 2 so address 2 is discovered via gap limit scanning rather than being forcefully included - Remove redundant TronAddress::from_base58 validations since assert_eq! against known addresses already validates them - Check for strictly zero balance instead of < 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add spaced version for `not_enough_effective_connection` detection
- Add source code links for TRON error formats documentation
- Remove undocumented error formats that TRON doesn't return:
- `{"error": "message"}` string variant
- `{"code": ..., "message": ...}` without result wrapper
- Update inline comments to be accurate
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use `.starts_with()` with exact response_code names instead of loose `.contains()` matching for transient error detection - Add documentation explaining why string codes are used (TRON's JsonFormat serializes enums as names, not numeric values) - Define RATE_LIMIT_MSG constant for consistency with RETRYABLE_CODES - Add source code links for all error handling logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed from struct with optional fields to untagged enum with Account and NoAccount variants - `address` is required field (always present for existing accounts) - `balance` and `create_time` use #[serde(default)] for proto3 defaults - Simplified exists_meaningfully() to just check for Account variant - Added comprehensive documentation about proto3 serialization and extensibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TRON's protobuf uses int64, but balance can never be negative. Using u64 ensures we get a deserialization error if the API ever returns an invalid negative value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| /// Notes: | ||
| /// - Returns `Err(TaskEnableError::RpcError(_))` if the task finishes with `status=Error` | ||
| /// or `status=UserActionRequired`. | ||
| /// - Returns `Err(TaskEnableError::Timeout{..})` if timeout is exceeded. |
There was a problem hiding this comment.
nit: why removing these notes?
There was a problem hiding this comment.
the notes documented standard task-enable behavior that's self-evident from the signature, kept the doc concise
mm2src/coins/eth/tron/api.rs
Outdated
| // See: https://github.com/tronprotocol/java-tron/blob/1e35f79/framework/src/main/java/org/tron/core/services/http/RateLimiterServlet.java#L114 | ||
| const RATE_LIMIT_MSG: &str = "lack of computing resources"; | ||
|
|
||
| if RETRYABLE_CODES.iter().any(|code| error_msg.starts_with(code)) { |
There was a problem hiding this comment.
are we sure then that the error_msg here will always start with the code? 🤔
maybe we wanna use .contains() instead
There was a problem hiding this comment.
addressed in 2c2a0df - we now parse TRON responses into TronErrorPayload { code, message } struct and check the code field directly, no string parsing needed
mm2src/coins/eth/tron/api.rs
Outdated
| /// Owner permission structure (present for activated accounts). | ||
| #[serde(default)] | ||
| pub owner_permission: Option<serde_json::Value>, |
There was a problem hiding this comment.
why did this field get removed?
nothing mentioned about it in the commit message.
There was a problem hiding this comment.
refactored GetAccountResponse to untagged enum in 50e8af8 - kept only fields needed for HD activation (address, balance, create_time). owner_permission was used in exists_meaningfully() which is now just matches!(self, Account { .. }). see Extensibility doc comment - additional fields like owner_permission can be added as needed
Error Classification at Source:
- Add `TronErrorPayload` struct with separate code and message fields
- `is_retryable()` method identifies transient TRON conditions:
- SERVER_BUSY, NO_CONNECTION, NOT_ENOUGH_EFFECTIVE_CONNECTION, BLOCK_UNSOLIDIFIED
- Rate limiting: "lack of computing resources"
- Non-retryable errors (CONTRACT_VALIDATE_ERROR, SIGERROR, etc.) fail immediately
- Sources: java-tron api.proto, RateLimiterServlet.java
New Web3RpcError Variants:
- `BadResponse`: retryable, for malformed/unexpected JSON from faulty nodes
(triggers node rotation to find a healthy node)
- `RemoteError { code, message }`: non-retryable, structured TRON rejections
(deterministic failures that would fail on any node)
Security Fix:
- `TronEmptyObject` with `#[serde(deny_unknown_fields)]` prevents error payloads
like `{"code":"...", "message":"..."}` from deserializing as `NoAccount`,
which would silently bypass error handling
MmResult Migration:
- `current_block()`, `balance_native_tron()`, `is_address_used_tron()` now
return `MmResult` for proper trace preservation using `mm_err()`
From Impl Updates:
- All `From<Web3RpcError>` impls handle BadResponse and RemoteError
- RemoteError collapsed to Transport with `format_remote_error()` for
chain-agnostic error types (BalanceError, WithdrawError, etc.)
Codebase Conventions:
- `serde_json::Value as Json`, `serde_json::{self as json}` aliases
- `impl Display` instead of `to_display_string()` method
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Brings in PR #2707 (docker tests split into feature-gated parallel suites). Resolved conflicts: - mm2src/coins/eth.rs: kept TRON rpc_client field, used dev's payment function auto-detection - mm2src/mm2_main/AGENTS.md: kept both TRON test docs and docker test infrastructure docs - mm2src/mm2_main/Cargo.toml: kept tron-network-tests feature, added all docker-tests-* features - docker test files: used dev's restructured versions, re-applied TRON type changes Applied TRON-compatible fixes to new docker test structure: - Updated task_enable_eth_with_tokens calls to use Some(&swap_contract) for optional parameter - Updated address handling to use .inner() for ChainTaggedAddress -> H160 conversion - Moved tron-network-tests feature above run-docker-tests for consistency
Two issues were causing unit tests to fail after merging dev into tron-hd-activation: 1. **ERC20 test helper missing platform coin registration** The TRON branch changed `get_swap_gas_fee_policy()` test stub to use the actual `swap_gas_fee_policy` field instead of returning default (commit a706c15 - to remove dead_code warning). This change calls `platform_coin()` which for ERC20 tokens does `lp_coinfind_or_err()` to look up the platform coin by ticker. The test helper `eth_coin_from_keypair()` creates ERC20 coins with `platform: "ETH"` but never registered an ETH coin in the context, causing `CoinFindError::NoSuchCoin` when tests called functions that use `get_swap_gas_fee_policy()`. Fix: Refactored `for_tests.rs` to extract coin creation into `make_eth_coin()` helper and register the platform coin in context before creating ERC20/NFT tokens. Call path that was failing: - test calls `get_sender_trade_fee()` - -> `get_swap_gas_fee_policy()` - -> `platform_coin()` - -> `lp_coinfind_or_err(&ctx, "ETH")` -> FAIL 2. **NFT error messages using Display format** Updated NFT error messages to use Display format (`{}`) instead of Debug (`{:?}`) or hardcoded strings. The TRON branch implemented `Display` for `EthCoinType` which shows user-friendly output like "NFT on ETH" instead of `Nft { platform: "ETH" }`. Also fixed `self.coin_type` -> `coin.coin_type` in async block at `address_balance()` where `self` is not available. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix test failures caused by get_swap_gas_fee_policy() calling platform_coin(), which does lp_coinfind_or_err() lookup requiring the platform to be registered in context. Changes: - helpers/eth.rs: Register ETH in MM_CTX before creating ERC20 token in erc20_coin_with_random_privkey() - eth_docker_tests.rs: Register ETH in MM_CTX1 before creating NFT coin in global_nft_with_random_privkey() Both use lp_coinfind().ok().flatten().is_none() check before registration since these are shared static contexts that may already have the platform registered from previous test calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When tests run in parallel, multiple tests may try to register the same platform coin simultaneously. The lp_coinfind check passes for both, but add_platform_with_tokens fails for the second one with PlatformIsAlreadyActivatedErr. Fix by ignoring the error from add_platform_with_tokens since it's safe - the platform is already registered by another test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| // Our error detection should now catch the nested result error | ||
| let result: Result<serde_json::Value, _> = client.post("/wallet/triggerconstantcontract", &request).await; | ||
|
|
||
| // Should be an error because our error detection catches nested {"result": {"result": false, ...}} | ||
| assert!(result.is_err(), "Expected error for non-existent contract"); | ||
|
|
||
| let error_str = format!("{:?}", result.unwrap_err()); | ||
| println!("Caught nested result error: {}", error_str); | ||
|
|
||
| // Verify the error message contains the expected information | ||
| assert!( | ||
| error_str.contains("CONTRACT_VALIDATE_ERROR") || error_str.contains("Smart contract"), | ||
| "Error should mention CONTRACT_VALIDATE_ERROR or 'Smart contract': {}", | ||
| error_str | ||
| ); |
There was a problem hiding this comment.
with the latest changes, it would be nice if we assert to the structure of the error (code, message, being TronErrorPayload, RemoteError, etc...) instead of just converting it to string and do contains() check.
There was a problem hiding this comment.
sorry i wasn't fully clear. this annotation was just an example, i mean we should do such a thing for all other tron api tests. i see there are yet some other ones that we don't assert their structure.
so basically tests that have .is_err() or .unwrap_err().
There was a problem hiding this comment.
Done in 8c5867f
Applied structured Web3RpcError assertions to the remaining tests. For test_error_invalid_endpoint_impl we verify it's a Transport with the HTTP status message format ("TRON API returned status 405"). For test_all_nodes_failing_returns_transport_error_impl we verify it's a Transport error from the connection failure path.
Move `GetAccountResponse` documentation from `TronEmptyObject` to the actual enum. `TronEmptyObject` now has a brief doc explaining its purpose and `deny_unknown_fields` requirement. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update test_error_nested_result_detection to assert on Web3RpcError::RemoteError
structure instead of formatting error as string and using contains check.
- Assert code is Some("CONTRACT_VALIDATE_ERROR")
- Assert message contains "Smart contract"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Assert that TRON API errors are correctly classified into Web3RpcError variants: - test_error_invalid_endpoint_impl: Verify HTTP 405 response is classified as Transport with "TRON API returned status 405" message format - test_all_nodes_failing_returns_transport_error_impl: Verify connection failures are classified as Transport (via "Request failed:" path) Previously these tests only checked is_err() without verifying the error classification. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add transaction signing, fee estimation, and the full withdraw pipeline for TRX native and TRC20 token transfers. Fourth PR in the TRON integration series (#2425, #2467, #2712). New modules under coins/eth/tron/: - proto.rs: minimal TRON protobuf types (prost::Message derive) - tx_builder.rs: unsigned transaction builders with TAPOS reference block - sign.rs: SHA256 + secp256k1 signing with 65-byte r||s||v signatures - fee.rs: bandwidth/energy fee estimation with chain price lookup - withdraw.rs: TRX (with iterative max-send convergence) and TRC20 (with cross-asset TRX balance verification) withdraw flows Key changes: - Chain-dispatched broadcast: TRON protobuf → /wallet/broadcasthex, EVM RLP → eth_sendRawTransaction - New TxFeeDetails::Tron variant with bandwidth_used, energy_used, bandwidth_fee, energy_fee, total_fee - TxFeeDetails extracted from lp_coins.rs into its own module - Optional expiration_seconds on WithdrawRequest (default 60s) - get_account_resource and broadcast_hex RPC methods on TronApiClient - 7 withdraw integration tests against Nile testnet - 24 unit tests converted to cross_test! for WASM compatibility Partially addresses #1542 and #1698.
Summary
Complete implementation of TRON (TRX) HD wallet activation through the existing ETH activation pipeline (
enable_eth_with_tokens/task::enable_eth::*).Key Features
T...(Base58Check) address formattingArchitecture
EthCointype (TRON/ETH keypairs are compatible at byte level)ChainRpcClientabstraction with explicit chain dispatch (nois_some()traps)ChainTaggedAddresswrapper for chain-aware address formattingTronApiClientwith native + WASM HTTP implementationsTest Coverage
10 integration tests (
cargo test --test mm2_tests_main --features tron-network-tests tron_):test_trx_activation_immediatetest_trx_activation_task_basedtest_trx_activation_node_failovertest_trx_activation_all_nodes_deadtest_trx_hd_activation_with_pathtest_trx_get_new_address_rpc_hdtest_trx_hd_balance_structure_assertions_and_funded_amountstest_trx_hd_multiple_account_ids_account_77test_trx_hd_gap_limit_scanning_finds_index_7_after_unfunded_gapstest_trx_hd_scanning_detects_used_but_zero_balance_addressCoin Configuration
{ "coin": "TRX", "name": "TRON", "fname": "tron", "mm2": 1, "wallet_only": true, "decimals": 6, "avg_blocktime": 3, "required_confirmations": 1, "protocol": { "type": "TRX", "protocol_data": { "network": "Mainnet" } }, "derivation_path": "m/44'/195'" }Activation Request
Out of Scope (Future Work)
Review Focus Areas
mm2src/coins/eth/chain_rpc.rs,chain_address.rs) - explicit chain dispatch patternmm2src/coins/eth/tron/api.rs) - native + WASM implementationsmm2src/coins/eth/eth_hd_wallet.rs) - chain-aware address formattingmm2src/coins/eth/v2_activation.rs) - wallet-only TRX handlingmm2src/mm2_main/tests/mm2_tests/tron_tests.rs) - integration tests🤖 Generated with Claude Code