mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-03-13 11:36:15 +01:00
Bech32: refactor, support arbitrary byte[] encodings
* Move/refactor utility methods from `SegwitAddress` to `Bech32` * Add `Bech32.Bech32Bytes` class for wrapping `Bech32` 5-bit byte arrays * Have existing `Bech32.Bech32Data` class extend `Bech32.Bech32Bytes` * Add `encodeBytes` and `decodeBytes` to `Bech32` for encoding/decoding arbitrary `byte[]` * Add tests for Nostr NIP-19 test vectors
This commit is contained in:
parent
539d25b35a
commit
7e073f1793
4 changed files with 239 additions and 86 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2018 Coinomi Ltd
|
||||
* Copyright by the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,17 +17,24 @@
|
|||
package org.bitcoinj.base;
|
||||
|
||||
import org.bitcoinj.base.exceptions.AddressFormatException;
|
||||
import org.bitcoinj.base.internal.ByteArray;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.bitcoinj.base.internal.Preconditions.checkArgument;
|
||||
|
||||
/**
|
||||
* Implementation of the Bech32 encoding.
|
||||
* <p>See <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP350</a> and
|
||||
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP173</a> for details.
|
||||
* Implementation of the Bech32 encoding. Used in the implementation of {@link SegwitAddress} and
|
||||
* also provides an API for encoding/decoding arbitrary Bech32 data. To parse Bech32 Bitcoin addresses,
|
||||
* use {@link AddressParser}. To encode arbitrary Bech32 data, see {@link #encodeBytes(Encoding, String, byte[])}.
|
||||
* To decode arbitrary Bech32 strings, see {@link #decodeBytes(String, String, Encoding)} or {@link #decode(String)}.
|
||||
* <p>
|
||||
* Based on the original Coinomi implementation.
|
||||
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki">BIP173</a>
|
||||
* @see <a href="https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki">BIP350</a>
|
||||
*/
|
||||
public class Bech32 {
|
||||
/** The Bech32 character set for encoding. */
|
||||
|
@ -54,17 +61,103 @@ public class Bech32 {
|
|||
public enum Encoding { BECH32, BECH32M }
|
||||
|
||||
/**
|
||||
* Bech32 decoded data in 5-bit byte format. Typically, the result of {@link #decode(String)}.
|
||||
* Binary data in 5-bits-per-byte format as used in Bech32 encoding/decoding.
|
||||
*/
|
||||
public static class Bech32Data {
|
||||
public static class Bech32Bytes extends ByteArray {
|
||||
/**
|
||||
* Wrapper for a {@code byte[]} array.
|
||||
*
|
||||
* @param bytes bytes to be copied (5-bits per byte format)
|
||||
*/
|
||||
protected Bech32Bytes(byte[] bytes) {
|
||||
super(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an instance, from two parts. Useful for the Segwit implementation,
|
||||
* see {@link #ofSegwit(short, byte[])}.
|
||||
* @param first first byte (5-bits per byte format)
|
||||
* @param rest remaining bytes (5-bits per byte format)
|
||||
*/
|
||||
private Bech32Bytes(byte first, byte[] rest) {
|
||||
super(concat(first, rest));
|
||||
}
|
||||
|
||||
private static byte[] concat(byte first, byte[] rest) {
|
||||
byte[] bytes = new byte[rest.length + 1];
|
||||
bytes[0] = first;
|
||||
System.arraycopy(rest, 0, bytes, 1, rest.length);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from arbitrary data, converts from 8-bits per byte
|
||||
* format to 5-bits per byte format before construction.
|
||||
* @param data arbitrary byte array (8-bits of data per byte)
|
||||
* @return Bech32 instance containing 5-bit encoding
|
||||
*/
|
||||
static Bech32Bytes ofBytes(byte[] data) {
|
||||
return new Bech32Bytes(encode8to5(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance from Segwit address binary data.
|
||||
* @param witnessVersion A short containing (5-bit) witness version information
|
||||
* @param witnessProgram a witness program (8-bits-per byte)
|
||||
* @return Bech32 instance containing 5-bit encoding
|
||||
*/
|
||||
static Bech32Bytes ofSegwit(short witnessVersion, byte[] witnessProgram) {
|
||||
// convert witnessVersion, witnessProgram to 5-bit Bech32Bytes
|
||||
return new Bech32Bytes((byte) (witnessVersion & 0xff), encode8to5(witnessProgram));
|
||||
}
|
||||
|
||||
private static byte[] encode8to5(byte[] data) {
|
||||
return convertBits(data, 0, data.length, 8, 5, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data, fully-decoded with 8-bits per byte.
|
||||
* @return The data, fully-decoded as a byte array.
|
||||
*/
|
||||
public byte[] decode5to8() {
|
||||
return convertBits(bytes, 0, bytes.length, 5, 8, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first byte (witness version if instance is a Segwit address)
|
||||
*/
|
||||
short witnessVersion() {
|
||||
return bytes[0];
|
||||
}
|
||||
|
||||
// Trim the version byte and return the witness program only
|
||||
private Bech32Bytes stripFirst() {
|
||||
byte[] program = new byte[bytes.length - 1];
|
||||
System.arraycopy(bytes, 1, program, 0, program.length);
|
||||
return new Bech32Bytes(program);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assuming this instance contains a Segwit address, return the witness program portion of the data.
|
||||
* @return The witness program as a byte array
|
||||
*/
|
||||
byte[] witnessProgram() {
|
||||
return stripFirst().decode5to8();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bech32 data in 5-bit byte format with {@link Encoding} and human-readable part (HRP) information.
|
||||
* Typically, the result of {@link #decode(String)}.
|
||||
*/
|
||||
public static class Bech32Data extends Bech32Bytes {
|
||||
public final Encoding encoding;
|
||||
public final String hrp;
|
||||
public final byte[] data;
|
||||
|
||||
private Bech32Data(final Encoding encoding, final String hrp, final byte[] data) {
|
||||
super(data);
|
||||
this.encoding = encoding;
|
||||
this.hrp = hrp;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,8 +190,8 @@ public class Bech32 {
|
|||
}
|
||||
|
||||
/** Verify a checksum. */
|
||||
private static @Nullable
|
||||
Encoding verifyChecksum(final String hrp, final byte[] values) {
|
||||
@Nullable
|
||||
private static Encoding verifyChecksum(final String hrp, final byte[] values) {
|
||||
byte[] hrpExpanded = expandHrp(hrp);
|
||||
byte[] combined = new byte[hrpExpanded.length + values.length];
|
||||
System.arraycopy(hrpExpanded, 0, combined, 0, hrpExpanded.length);
|
||||
|
@ -126,20 +219,57 @@ public class Bech32 {
|
|||
return ret;
|
||||
}
|
||||
|
||||
/** Encode a Bech32 string. */
|
||||
public static String encode(final Bech32Data bech32) {
|
||||
return encode(bech32.encoding, bech32.hrp, bech32.data);
|
||||
/**
|
||||
* Encode a byte array to a Bech32 string
|
||||
* @param encoding Desired encoding Bech32 or Bech32m
|
||||
* @param hrp human-readable part to use for encoding
|
||||
* @param bytes Arbitrary binary data (8-bits per byte)
|
||||
* @return A Bech32 string
|
||||
*/
|
||||
public static String encodeBytes(Encoding encoding, String hrp, byte[] bytes) {
|
||||
return encode(encoding, hrp, Bech32Bytes.ofBytes(bytes));
|
||||
}
|
||||
|
||||
/** Encode a Bech32 string. */
|
||||
public static String encode(Encoding encoding, final String hrp, final byte[] values) {
|
||||
/**
|
||||
* Decode a Bech32 string to a byte array.
|
||||
* @param bech32 A Bech32 format string
|
||||
* @param expectedHrp Expected value for the human-readable part
|
||||
* @param expectedEncoding Expected encoding
|
||||
* @return Decoded value as byte array (8-bits per byte)
|
||||
* @throws AddressFormatException if unexpected hrp or encoding
|
||||
*/
|
||||
public static byte[] decodeBytes(String bech32, String expectedHrp, Encoding expectedEncoding) {
|
||||
Bech32.Bech32Data decoded = decode(bech32);
|
||||
if (!decoded.hrp.equals(expectedHrp) || decoded.encoding != expectedEncoding) {
|
||||
throw new AddressFormatException("unexpected hrp or encoding");
|
||||
}
|
||||
return decoded.decode5to8();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a Bech32 string.
|
||||
* @param bech32 Contains 5-bits/byte data, desired encoding and human-readable part
|
||||
* @return A string containing the Bech32-encoded data
|
||||
*/
|
||||
public static String encode(final Bech32Data bech32) {
|
||||
return encode(bech32.encoding, bech32.hrp, bech32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a Bech32 string.
|
||||
* @param encoding The requested encoding
|
||||
* @param hrp The requested human-readable part
|
||||
* @param values Binary data in 5-bit per byte format
|
||||
* @return A string containing the Bech32-encoded data
|
||||
*/
|
||||
public static String encode(Encoding encoding, String hrp, Bech32Bytes values) {
|
||||
checkArgument(hrp.length() >= 1, () -> "human-readable part is too short: " + hrp.length());
|
||||
checkArgument(hrp.length() <= 83, () -> "human-readable part is too long: " + hrp.length());
|
||||
String lcHrp = hrp.toLowerCase(Locale.ROOT);
|
||||
byte[] checksum = createChecksum(encoding, lcHrp, values);
|
||||
byte[] combined = new byte[values.length + checksum.length];
|
||||
System.arraycopy(values, 0, combined, 0, values.length);
|
||||
System.arraycopy(checksum, 0, combined, values.length, checksum.length);
|
||||
byte[] checksum = createChecksum(encoding, lcHrp, values.bytes());
|
||||
byte[] combined = new byte[values.bytes().length + checksum.length];
|
||||
System.arraycopy(values.bytes(), 0, combined, 0, values.bytes().length);
|
||||
System.arraycopy(checksum, 0, combined, values.bytes().length, checksum.length);
|
||||
StringBuilder sb = new StringBuilder(lcHrp.length() + 1 + combined.length);
|
||||
sb.append(lcHrp);
|
||||
sb.append('1');
|
||||
|
@ -149,7 +279,14 @@ public class Bech32 {
|
|||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Decode a Bech32 string. */
|
||||
/**
|
||||
* Decode a Bech32 string.
|
||||
* <p>
|
||||
* To get the fully-decoded data, call {@link Bech32Bytes#decode5to8()} on the returned {@code Bech32Data}.
|
||||
* @param str A string containing Bech32-encoded data
|
||||
* @return An object with the detected encoding, hrp, and decoded data (in 5-bit per byte format)
|
||||
* @throws AddressFormatException if the string is invalid
|
||||
*/
|
||||
public static Bech32Data decode(final String str) throws AddressFormatException {
|
||||
boolean lower = false, upper = false;
|
||||
if (str.length() < 8)
|
||||
|
@ -185,4 +322,36 @@ public class Bech32 {
|
|||
if (encoding == null) throw new AddressFormatException.InvalidChecksum();
|
||||
return new Bech32Data(encoding, hrp, Arrays.copyOfRange(values, 0, values.length - 6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for re-arranging bits into groups.
|
||||
*/
|
||||
private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
|
||||
final int toBits, final boolean pad) throws AddressFormatException {
|
||||
int acc = 0;
|
||||
int bits = 0;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
|
||||
final int maxv = (1 << toBits) - 1;
|
||||
final int max_acc = (1 << (fromBits + toBits - 1)) - 1;
|
||||
for (int i = 0; i < inLen; i++) {
|
||||
int value = in[i + inStart] & 0xff;
|
||||
if ((value >>> fromBits) != 0) {
|
||||
throw new AddressFormatException(
|
||||
String.format("Input value '%X' exceeds '%d' bit size", value, fromBits));
|
||||
}
|
||||
acc = ((acc << fromBits) | value) & max_acc;
|
||||
bits += fromBits;
|
||||
while (bits >= toBits) {
|
||||
bits -= toBits;
|
||||
out.write((acc >>> bits) & maxv);
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits > 0)
|
||||
out.write((acc << (toBits - bits)) & maxv);
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||
throw new AddressFormatException("Could not convert bits, invalid padding");
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,12 +129,13 @@ class DefaultAddressParserProvider implements AddressParser.AddressParserProvide
|
|||
*/
|
||||
private SegwitAddress parseBech32AnyNetwork(String bech32)
|
||||
throws AddressFormatException {
|
||||
String hrp = Bech32.decode(bech32).hrp;
|
||||
return segwitNetworks.stream()
|
||||
Bech32.Bech32Data bechData = Bech32.decode(bech32);
|
||||
String hrp = bechData.hrp;
|
||||
Network network = segwitNetworks.stream()
|
||||
.filter(n -> hrp.equals(n.segwitAddressHrp()))
|
||||
.findFirst()
|
||||
.map(n -> SegwitAddress.fromBech32(bech32, n))
|
||||
.orElseThrow(() -> new AddressFormatException.InvalidPrefix("No network found for " + bech32));
|
||||
return SegwitAddress.fromBechData(network, bechData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.bitcoinj.core.NetworkParameters;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
|
@ -135,14 +134,6 @@ public class SegwitAddress implements Address {
|
|||
return network;
|
||||
}
|
||||
|
||||
private static byte[] encode8to5(byte[] data) {
|
||||
return convertBits(data, 0, data.length, 8, 5, true);
|
||||
}
|
||||
|
||||
private static byte[] decode5to8(byte[] data) {
|
||||
return convertBits(data, 0, data.length, 5, 8, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor. Use {@link #fromBech32(String, Network)},
|
||||
* {@link #fromHash(Network, byte[])} or {@link ECKey#toAddress(ScriptType, Network)}.
|
||||
|
@ -276,13 +267,12 @@ public class SegwitAddress implements Address {
|
|||
throw new AddressFormatException.WrongNetwork(bechData.hrp);
|
||||
}
|
||||
|
||||
private static SegwitAddress fromBechData(Network network, Bech32.Bech32Data bechData) {
|
||||
if (bechData.data.length < 1) {
|
||||
static SegwitAddress fromBechData(@Nonnull Network network, Bech32.Bech32Data bechData) {
|
||||
if (bechData.bytes().length < 1) {
|
||||
throw new AddressFormatException.InvalidDataLength("invalid address length (0)");
|
||||
}
|
||||
final int witnessVersion = bechData.data[0];
|
||||
byte[] witnessProgram = decode5to8(trimVersion(bechData.data));
|
||||
final SegwitAddress address = new SegwitAddress(network, witnessVersion, witnessProgram);
|
||||
final int witnessVersion = bechData.witnessVersion();
|
||||
final SegwitAddress address = new SegwitAddress(network, witnessVersion, bechData.witnessProgram());
|
||||
if ((witnessVersion == 0 && bechData.encoding != Bech32.Encoding.BECH32) ||
|
||||
(witnessVersion != 0 && bechData.encoding != Bech32.Encoding.BECH32M))
|
||||
throw new AddressFormatException.UnexpectedWitnessVersion("Unexpected witness version: " + witnessVersion);
|
||||
|
@ -396,54 +386,7 @@ public class SegwitAddress implements Address {
|
|||
*/
|
||||
public String toBech32() {
|
||||
Bech32.Encoding encoding = (witnessVersion == 0) ? Bech32.Encoding.BECH32 : Bech32.Encoding.BECH32M;
|
||||
return Bech32.encode(encoding, network.segwitAddressHrp(), appendVersion(witnessVersion, encode8to5(witnessProgram)));
|
||||
}
|
||||
|
||||
// Trim the version byte and return the witness program only
|
||||
private static byte[] trimVersion(byte[] data) {
|
||||
byte[] program = new byte[data.length - 1];
|
||||
System.arraycopy(data, 1, program, 0, program.length);
|
||||
return program;
|
||||
}
|
||||
|
||||
// concatenate the witnessVersion and witnessProgram
|
||||
private static byte[] appendVersion(short version, byte[] program) {
|
||||
byte[] data = new byte[program.length + 1];
|
||||
data[0] = (byte) version;
|
||||
System.arraycopy(program, 0, data, 1, program.length);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for re-arranging bits into groups.
|
||||
*/
|
||||
private static byte[] convertBits(final byte[] in, final int inStart, final int inLen, final int fromBits,
|
||||
final int toBits, final boolean pad) throws AddressFormatException {
|
||||
int acc = 0;
|
||||
int bits = 0;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(64);
|
||||
final int maxv = (1 << toBits) - 1;
|
||||
final int max_acc = (1 << (fromBits + toBits - 1)) - 1;
|
||||
for (int i = 0; i < inLen; i++) {
|
||||
int value = in[i + inStart] & 0xff;
|
||||
if ((value >>> fromBits) != 0) {
|
||||
throw new AddressFormatException(
|
||||
String.format("Input value '%X' exceeds '%d' bit size", value, fromBits));
|
||||
}
|
||||
acc = ((acc << fromBits) | value) & max_acc;
|
||||
bits += fromBits;
|
||||
while (bits >= toBits) {
|
||||
bits -= toBits;
|
||||
out.write((acc >>> bits) & maxv);
|
||||
}
|
||||
}
|
||||
if (pad) {
|
||||
if (bits > 0)
|
||||
out.write((acc << (toBits - bits)) & maxv);
|
||||
} else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv) != 0) {
|
||||
throw new AddressFormatException("Could not convert bits, invalid padding");
|
||||
}
|
||||
return out.toByteArray();
|
||||
return Bech32.encode(encoding, network.segwitAddressHrp(), Bech32.Bech32Bytes.ofSegwit(witnessVersion, witnessProgram));
|
||||
}
|
||||
|
||||
// Comparator for SegwitAddress, left argument must be SegwitAddress, right argument can be any Address
|
||||
|
|
|
@ -16,14 +16,19 @@
|
|||
|
||||
package org.bitcoinj.base;
|
||||
|
||||
import junitparams.JUnitParamsRunner;
|
||||
import junitparams.Parameters;
|
||||
import org.bitcoinj.base.exceptions.AddressFormatException;
|
||||
import org.bitcoinj.base.internal.ByteUtils;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@RunWith(JUnitParamsRunner.class)
|
||||
public class Bech32Test {
|
||||
@Test
|
||||
public void valid_bech32() {
|
||||
|
@ -43,7 +48,7 @@ public class Bech32Test {
|
|||
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
|
||||
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
|
||||
// Test encoding with an uppercase HRP
|
||||
recode = Bech32.encode(bechData.encoding, bechData.hrp.toUpperCase(Locale.ROOT), bechData.data);
|
||||
recode = Bech32.encode(bechData.encoding, bechData.hrp.toUpperCase(Locale.ROOT), bechData);
|
||||
assertEquals(String.format("Failed to roundtrip '%s' -> '%s'", valid, recode),
|
||||
valid.toLowerCase(Locale.ROOT), recode.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
@ -120,6 +125,41 @@ public class Bech32Test {
|
|||
"1p2gdwpf", // empty HRP
|
||||
};
|
||||
|
||||
@Test
|
||||
@Parameters(method = "nip19Vectors")
|
||||
public void encodeBytes(String hex, String hrp, String expectedBech32) {
|
||||
String bech32 = Bech32.encodeBytes(Bech32.Encoding.BECH32, hrp, ByteUtils.parseHex(hex));
|
||||
assertEquals("incorrect encoding", expectedBech32, bech32);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "nip19Vectors")
|
||||
public void decodeBytes(String expectedHex, String hrp, String bech32) {
|
||||
Bech32.Bech32Data decoded = Bech32.decode(bech32);
|
||||
String decodedData = ByteUtils.formatHex(decoded.decode5to8());
|
||||
|
||||
assertEquals("incorrect encoding type", Bech32.Encoding.BECH32, decoded.encoding);
|
||||
assertEquals("incorrect hrp", hrp, decoded.hrp);
|
||||
assertEquals("incorrect decoded data", expectedHex, decodedData);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Parameters(method = "nip19Vectors")
|
||||
public void decodeBytes2(String expectedHex, String hrp, String bech32) {
|
||||
byte[] decoded = Bech32.decodeBytes(bech32, hrp, Bech32.Encoding.BECH32);
|
||||
|
||||
assertEquals("incorrect decoded data", expectedHex, ByteUtils.formatHex(decoded));
|
||||
}
|
||||
|
||||
// These vectors are from NIP-19: https://github.com/nostr-protocol/nips/blob/master/19.md
|
||||
private Object[] nip19Vectors() {
|
||||
return new Object[]{
|
||||
new Object[]{ "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "npub", "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"},
|
||||
new Object[]{ "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e", "npub", "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg"},
|
||||
new Object[]{ "67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa", "nsec", "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"},
|
||||
};
|
||||
}
|
||||
|
||||
@Test(expected = AddressFormatException.InvalidCharacter.class)
|
||||
public void decode_invalidCharacter_notInAlphabet() {
|
||||
Bech32.decode("A12OUEL5X");
|
||||
|
|
Loading…
Add table
Reference in a new issue