From 0dc67f06c4d657a04d34119ea643d3496d82fc06 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 10 Aug 2025 15:56:23 -0400 Subject: [PATCH] support Cardano (ADA) --- .../haveno/asset/CardanoAddressValidator.java | 106 ++++++++++++++++++ .../main/java/haveno/asset/coins/Cardano.java | 28 +++++ .../META-INF/services/haveno.asset.Asset | 1 + .../java/haveno/asset/coins/CardanoTest.java | 42 +++++++ .../java/haveno/core/locale/CurrencyUtil.java | 1 + .../src/main/java/haveno/desktop/images.css | 4 + .../src/main/resources/images/ada_logo.png | Bin 0 -> 14706 bytes 7 files changed, 182 insertions(+) create mode 100644 assets/src/main/java/haveno/asset/CardanoAddressValidator.java create mode 100644 assets/src/main/java/haveno/asset/coins/Cardano.java create mode 100644 assets/src/test/java/haveno/asset/coins/CardanoTest.java create mode 100644 desktop/src/main/resources/images/ada_logo.png diff --git a/assets/src/main/java/haveno/asset/CardanoAddressValidator.java b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java new file mode 100644 index 0000000000..0b3546e4fb --- /dev/null +++ b/assets/src/main/java/haveno/asset/CardanoAddressValidator.java @@ -0,0 +1,106 @@ +/* + * 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; + +/** + * Validates a Shelley-era mainnet Cardano address. + */ +public class CardanoAddressValidator extends RegexAddressValidator { + + private static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + private static final int BECH32_CONST = 1; + private static final int BECH32M_CONST = 0x2bc830a3; + private static final int MAX_LEN = 104; // bech32 / bech32m max for Cardano + + public CardanoAddressValidator() { + super("^addr1[0-9a-z]{20,98}$"); + } + + public CardanoAddressValidator(String errorMessageI18nKey) { + super("^addr1[0-9a-z]{20,98}$", errorMessageI18nKey); + } + + @Override + public AddressValidationResult validate(String address) { + if (!isValidShelleyMainnet(address)) { + return AddressValidationResult.invalidStructure(); + } + return super.validate(address); + } + + /** + * Checks if the given address is a valid Shelley-era mainnet Cardano 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 isValidShelleyMainnet(String addr) { + if (addr == null) return false; + String lower = addr.toLowerCase(); + + // must start addr1 and not be absurdly long + if (!lower.startsWith("addr1") || lower.length() > MAX_LEN) return false; + + int sep = lower.lastIndexOf('1'); + if (sep < 1) return false; // no separator or empty HRP + String hrp = lower.substring(0, sep); + if (!"addr".equals(hrp)) return false; // mainnet only + + String dataPart = lower.substring(sep + 1); + if (dataPart.length() < 6) return false; // checksum is 6 chars minimum + + int[] data = new int[dataPart.length()]; + for (int i = 0; i < dataPart.length(); i++) { + int v = CHARSET.indexOf(dataPart.charAt(i)); + if (v == -1) return false; + data[i] = v; + } + + int[] hrpExp = hrpExpand(hrp); + int[] combined = new int[hrpExp.length + data.length]; + System.arraycopy(hrpExp, 0, combined, 0, hrpExp.length); + System.arraycopy(data, 0, combined, hrpExp.length, data.length); + + int chk = polymod(combined); + return chk == BECH32_CONST || chk == BECH32M_CONST; // accept either legacy Bech32 (1) or Bech32m (0x2bc830a3) + } + + private static int[] hrpExpand(String hrp) { + int[] ret = new int[hrp.length() * 2 + 1]; + int idx = 0; + for (char c : hrp.toCharArray()) ret[idx++] = c >> 5; + ret[idx++] = 0; + for (char c : hrp.toCharArray()) ret[idx++] = c & 31; + return ret; + } + + private static int polymod(int[] values) { + int chk = 1; + int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}; + for (int v : values) { + int b = chk >>> 25; + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (int i = 0; i < 5; i++) { + if (((b >>> i) & 1) != 0) chk ^= GEN[i]; + } + } + return chk; + } +} diff --git a/assets/src/main/java/haveno/asset/coins/Cardano.java b/assets/src/main/java/haveno/asset/coins/Cardano.java new file mode 100644 index 0000000000..16f2563930 --- /dev/null +++ b/assets/src/main/java/haveno/asset/coins/Cardano.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.CardanoAddressValidator; +import haveno.asset.Coin; + +public class Cardano extends Coin { + + public Cardano() { + super("Cardano", "ADA", new CardanoAddressValidator()); + } +} 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 a10943e769..a68e22a8d5 100644 --- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset +++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset @@ -4,6 +4,7 @@ # See https://haveno.exchange/list-asset for complete instructions. haveno.asset.coins.Bitcoin$Mainnet haveno.asset.coins.BitcoinCash +haveno.asset.coins.Cardano haveno.asset.coins.Ether haveno.asset.coins.Litecoin haveno.asset.coins.Monero diff --git a/assets/src/test/java/haveno/asset/coins/CardanoTest.java b/assets/src/test/java/haveno/asset/coins/CardanoTest.java new file mode 100644 index 0000000000..bae8141aba --- /dev/null +++ b/assets/src/test/java/haveno/asset/coins/CardanoTest.java @@ -0,0 +1,42 @@ +/* + * 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 CardanoTest extends AbstractAssetTest { + + public CardanoTest() { + super(new Cardano()); + } + + @Test + public void testValidAddresses() { + assertValidAddress("addr1vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); + assertValidAddress("addr1q8gg2r3vf9zggn48g7m8vx62rwf6warcs4k7ej8mdzmqmesj30jz7psduyk6n4n2qrud2xlv9fgj53n6ds3t8cs4fvzs05yzmz"); + } + + @Test + public void testInvalidAddresses() { + assertInvalidAddress("addr1Q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); + assertInvalidAddress("addr2q9r4y0gx0m4hd5s2u3pnj7ufc4s0ghqzj7u6czxyfks5cty5k5yq5qp6gmw5v7uqvx2g4kw6zjhx4l6fnhcey9lg9nys6v2mpu"); + assertInvalidAddress("addr2vpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5eg0yu80w"); + assertInvalidAddress("Ae2tdPwUPEYxkYw5GrFyqb4Z9TzXo8f1WnWpPZP1sXrEn1pz2VU3CkJ8aTQ"); + } +} diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java index e3b61eefaf..dc65c954ec 100644 --- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java +++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java @@ -201,6 +201,7 @@ public class CurrencyUtil { result.add(new CryptoCurrency("ETH", "Ether")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("XRP", "Ripple")); + result.add(new CryptoCurrency("ADA", "Cardano")); 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 549549605e..013a07e300 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -357,6 +357,10 @@ -fx-image: url("../../images/xrp_logo.png"); } +#image-ada-logo { + -fx-image: url("../../images/ada_logo.png"); +} + #image-dark-mode-toggle { -fx-image: url("../../images/dark_mode_toggle.png"); } diff --git a/desktop/src/main/resources/images/ada_logo.png b/desktop/src/main/resources/images/ada_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..70d1166426a8c4ca7cb101bf6cd1a26957ce5111 GIT binary patch literal 14706 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 literal 0 HcmV?d00001