support Cardano (ADA)

This commit is contained in:
woodser 2025-08-10 15:56:23 -04:00 committed by woodser
parent 7298a6373a
commit 0dc67f06c4
7 changed files with 182 additions and 0 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.CardanoAddressValidator;
import haveno.asset.Coin;
public class Cardano extends Coin {
public Cardano() {
super("Cardano", "ADA", new CardanoAddressValidator());
}
}

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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");
}
}

View file

@ -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"));

View file

@ -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");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB