Skip to content

feat(tron): add HD wallet activation via EthCoin pipeline#2467

Merged
shamardy merged 48 commits intodevfrom
tron-hd-activation
Jan 12, 2026
Merged

feat(tron): add HD wallet activation via EthCoin pipeline#2467
shamardy merged 48 commits intodevfrom
tron-hd-activation

Conversation

@laruh
Copy link
Copy Markdown

@laruh laruh commented May 26, 2025

Summary

Complete implementation of TRON (TRX) HD wallet activation through the existing ETH activation pipeline (enable_eth_with_tokens / task::enable_eth::*).

Key Features

  • HD wallet activation with correct T... (Base58Check) address formatting
  • Real TRON RPC integration - no stubs, uses actual Nile testnet
  • Gap limit scanning - correctly finds funded addresses after unfunded gaps
  • Used-but-zero-balance detection - identifies addresses with tx history but 0 balance
  • Node failover - rotation through multiple TRON nodes on failure
  • Wallet-only mode - TRX activation skips swap contract validation

Architecture

  • Reuses EthCoin type (TRON/ETH keypairs are compatible at byte level)
  • New ChainRpcClient abstraction with explicit chain dispatch (no is_some() traps)
  • ChainTaggedAddress wrapper for chain-aware address formatting
  • TronApiClient with native + WASM HTTP implementations

Test Coverage

10 integration tests (cargo test --test mm2_tests_main --features tron-network-tests tron_):

Test Coverage
test_trx_activation_immediate Immediate mode activation
test_trx_activation_task_based Task-based activation
test_trx_activation_node_failover Dead node → good node fallback
test_trx_activation_all_nodes_dead Error handling when all nodes fail
test_trx_hd_activation_with_path HD activation with derivation path
test_trx_get_new_address_rpc_hd get_new_address RPC for HD wallets
test_trx_hd_balance_structure_assertions_and_funded_amounts Balance structure validation
test_trx_hd_multiple_account_ids_account_77 Non-zero account ID (ETH parity)
test_trx_hd_gap_limit_scanning_finds_index_7_after_unfunded_gaps Gap limit scanning
test_trx_hd_scanning_detects_used_but_zero_balance_address Used but empty address detection

Coin 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

curl --url "http://127.0.0.1:7783" --data '{
  "userpass": "YOUR_USERPASS",
  "method": "enable_eth_with_tokens",
  "mmrpc": "2.0",
  "params": {
    "ticker": "TRX",
    "swap_contract_address": "0x0000000000000000000000000000000000000000",
    "nodes": [
      { "url": "https://nile.trongrid.io" }
    ],
    "erc20_tokens_requests": []
  }
}'

Out of Scope (Future Work)

  • TRC20 token activation
  • TRON transaction signing / withdraw / swaps
  • TRON fee estimation (bandwidth/energy)

Review Focus Areas

  1. Chain abstraction (mm2src/coins/eth/chain_rpc.rs, chain_address.rs) - explicit chain dispatch pattern
  2. TRON RPC client (mm2src/coins/eth/tron/api.rs) - native + WASM implementations
  3. HD wallet integration (mm2src/coins/eth/eth_hd_wallet.rs) - chain-aware address formatting
  4. Activation flow (mm2src/coins/eth/v2_activation.rs) - wallet-only TRX handling
  5. Test coverage (mm2src/mm2_main/tests/mm2_tests/tron_tests.rs) - integration tests

🤖 Generated with Claude Code

@laruh laruh changed the title feat(tron): tron HD activation support feat(tron): initial tron HD activation support May 26, 2025
@laruh laruh added feature: protocol priority: medium Moderately important tasks that should be completed but are not urgent. labels May 27, 2025
Copy link
Copy Markdown

@onur-ozkan onur-ozkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!
first iteration

onur-ozkan
onur-ozkan previously approved these changes May 28, 2025
Copy link
Copy Markdown

@onur-ozkan onur-ozkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you.

borngraced
borngraced previously approved these changes May 28, 2025
shamardy and others added 8 commits January 2, 2026 22:47
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>
Comment on lines -3828 to -3831
/// Notes:
/// - Returns `Err(TaskEnableError::RpcError(_))` if the task finishes with `status=Error`
/// or `status=UserActionRequired`.
/// - Returns `Err(TaskEnableError::Timeout{..})` if timeout is exceeded.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why removing these notes?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the notes documented standard task-enable behavior that's self-evident from the signature, kept the doc concise

// 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)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure then that the error_msg here will always start with the code? 🤔
maybe we wanna use .contains() instead

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed in 2c2a0df - we now parse TRON responses into TronErrorPayload { code, message } struct and check the code field directly, no string parsing needed

Comment on lines -359 to -361
/// Owner permission structure (present for activated accounts).
#[serde(default)]
pub owner_permission: Option<serde_json::Value>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did this field get removed?
nothing mentioned about it in the commit message.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@shamardy shamardy requested a review from mariocynicys January 7, 2026 05:40
shamardy and others added 4 commits January 7, 2026 11:59
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>
Comment on lines +295 to +309
// 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
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in eb98797

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

shamardy and others added 3 commits January 8, 2026 03:49
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>
@shamardy shamardy requested a review from mariocynicys January 8, 2026 02:26
shamardy and others added 2 commits January 9, 2026 08:49
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>
Copy link
Copy Markdown
Collaborator

@mariocynicys mariocynicys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, LGTM!

@shamardy shamardy merged commit 8404fa5 into dev Jan 12, 2026
35 checks passed
@shamardy shamardy deleted the tron-hd-activation branch January 12, 2026 13:56
shamardy added a commit that referenced this pull request Mar 6, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: protocol priority: urgent Critical tasks requiring immediate action. status: pending review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants