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 0000000000..70d1166426 Binary files /dev/null and b/desktop/src/main/resources/images/ada_logo.png differ