From 878cbb86cef154dffcb98cfb92f69002fa5f820d Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:34:20 -0400 Subject: [PATCH] support Tron (TRX) --- .../haveno/asset/TronAddressValidator.java | 104 ++++++++++++++++++ .../main/java/haveno/asset/coins/Tron.java | 28 +++++ .../META-INF/services/haveno.asset.Asset | 1 + .../java/haveno/asset/coins/TronTest.java | 45 ++++++++ .../java/haveno/core/locale/CurrencyUtil.java | 1 + .../src/main/java/haveno/desktop/images.css | 4 + .../src/main/resources/images/trx_logo.png | Bin 0 -> 11557 bytes 7 files changed, 183 insertions(+) create mode 100644 assets/src/main/java/haveno/asset/TronAddressValidator.java create mode 100644 assets/src/main/java/haveno/asset/coins/Tron.java create mode 100644 assets/src/test/java/haveno/asset/coins/TronTest.java create mode 100644 desktop/src/main/resources/images/trx_logo.png diff --git a/assets/src/main/java/haveno/asset/TronAddressValidator.java b/assets/src/main/java/haveno/asset/TronAddressValidator.java new file mode 100644 index 0000000000..6125975621 --- /dev/null +++ b/assets/src/main/java/haveno/asset/TronAddressValidator.java @@ -0,0 +1,104 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.util.Arrays; + +/** + * Validates a Tron address. + */ +public class TronAddressValidator implements AddressValidator { + + private static final String BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + private static final byte MAINNET_PREFIX = 0x41; + + public TronAddressValidator() { + } + + @Override + public AddressValidationResult validate(String address) { + if (!isValidTronAddress(address)) { + return AddressValidationResult.invalidStructure(); + } + return AddressValidationResult.validAddress(); + } + + /** + * Checks if the given address is a valid Solana address. + * + * This code is AI-generated and has been tested with a variety of addresses. + * + * @param addr the address to validate + * @return true if the address is valid, false otherwise + */ + private static boolean isValidTronAddress(String address) { + if (address == null || address.length() != 34) return false; + + byte[] decoded = decodeBase58(address); + if (decoded == null || decoded.length != 25) return false; // 21 bytes data + 4 bytes checksum + + // Check checksum + byte[] data = Arrays.copyOfRange(decoded, 0, 21); + byte[] checksum = Arrays.copyOfRange(decoded, 21, 25); + byte[] calculatedChecksum = Arrays.copyOfRange(doubleSHA256(data), 0, 4); + + if (!Arrays.equals(checksum, calculatedChecksum)) return false; + + // Check mainnet prefix + return data[0] == MAINNET_PREFIX; + } + + private static byte[] decodeBase58(String input) { + BigInteger num = BigInteger.ZERO; + BigInteger base = BigInteger.valueOf(58); + + for (char c : input.toCharArray()) { + int digit = BASE58_ALPHABET.indexOf(c); + if (digit < 0) return null; + num = num.multiply(base).add(BigInteger.valueOf(digit)); + } + + // Convert BigInteger to byte array + byte[] bytes = num.toByteArray(); + if (bytes.length > 1 && bytes[0] == 0) { + bytes = Arrays.copyOfRange(bytes, 1, bytes.length); + } + + // Add leading zero bytes for '1's + int leadingZeros = 0; + for (char c : input.toCharArray()) { + if (c == '1') leadingZeros++; + else break; + } + + byte[] result = new byte[leadingZeros + bytes.length]; + System.arraycopy(bytes, 0, result, leadingZeros, bytes.length); + return result; + } + + private static byte[] doubleSHA256(byte[] data) { + try { + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + return sha256.digest(sha256.digest(data)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Tron.java b/assets/src/main/java/haveno/asset/coins/Tron.java new file mode 100644 index 0000000000..d358834eeb --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Tron.java @@ -0,0 +1,28 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.Coin; +import haveno.asset.TronAddressValidator; + +public class Tron extends Coin { + + public Tron() { + super("Tron", "TRX", new TronAddressValidator()); + } +} diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset index 3125ac64aa..a42480de1d 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -10,6 +10,7 @@ haveno.asset.coins.Litecoin haveno.asset.coins.Monero haveno.asset.coins.Ripple haveno.asset.coins.Solana +haveno.asset.coins.Tron haveno.asset.tokens.TetherUSDERC20 haveno.asset.tokens.TetherUSDTRC20 haveno.asset.tokens.USDCoinERC20 diff --git a/assets/src/test/java/haveno/asset/coins/TronTest.java b/assets/src/test/java/haveno/asset/coins/TronTest.java new file mode 100644 index 0000000000..dd70d2edea --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/TronTest.java @@ -0,0 +1,45 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.asset.coins; + +import haveno.asset.AbstractAssetTest; +import org.junit.jupiter.api.Test; + +public class TronTest extends AbstractAssetTest { + + public TronTest() { + super(new Tron()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("TRjE1H8dxypKM1NZRdysbs9wo7huR4bdNz"); + assertValidAddress("THdUXD3mZqT5aMnPQMtBSJX9ANGjaeUwQK"); + assertValidAddress("THUE6WTLaEGytFyuGJQUcKc3r245UKypoi"); + assertValidAddress("TH7vVF9RTMXM9x7ZnPnbNcEph734hpu8cf"); + assertValidAddress("TJNtFduS4oebw3jgGKCYmgSpTdyPieb6Ha"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); + assertInvalidAddress("TJRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9X"); + assertInvalidAddress("1JRyWwFs9wTFGZg3L8nL62xwP9iK8QdK9R"); + assertInvalidAddress("TGzz8gjYiYRqpfmDwnLxfgPuLVNmpCswVo"); + } +} diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index 8d329324fe..58e7ab422b 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -203,6 +203,7 @@ public class CurrencyUtil { result.add(new CryptoCurrency("XRP", "Ripple")); result.add(new CryptoCurrency("ADA", "Cardano")); result.add(new CryptoCurrency("SOL", "Solana")); + result.add(new CryptoCurrency("TRX", "Tron")); result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin")); result.add(new CryptoCurrency("USDT-ERC20", "Tether USD")); result.add(new CryptoCurrency("USDT-TRC20", "Tether USD")); diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index cc388612b3..70ca246cd6 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -365,6 +365,10 @@ -fx-image: url("../../images/sol_logo.png"); } +#image-trx-logo { + -fx-image: url("../../images/trx_logo.png"); +} + #image-dark-mode-toggle { -fx-image: url("../../images/dark_mode_toggle.png"); } diff --git a/desktop/src/main/resources/images/trx_logo.png b/desktop/src/main/resources/images/trx_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bb2ab5742131b3e24023b11289189cd5be46edf7 GIT binary patch literal 11557 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 tslz)$DO)4?!(#ND94A&)Dh>DQna_B2)