
io.neow3j.contract.NeoNameService Maven / Gradle / Ivy
package io.neow3j.contract;
import io.neow3j.contract.exceptions.UnexpectedReturnTypeException;
import io.neow3j.protocol.Neow3j;
import io.neow3j.protocol.core.RecordType;
import io.neow3j.protocol.core.response.InvocationResult;
import io.neow3j.protocol.core.response.NameState;
import io.neow3j.protocol.core.stackitem.ByteStringStackItem;
import io.neow3j.protocol.core.stackitem.StackItem;
import io.neow3j.transaction.TransactionBuilder;
import io.neow3j.types.ContractParameter;
import io.neow3j.types.Hash160;
import io.neow3j.wallet.Account;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Map;
import java.util.regex.Pattern;
import static io.neow3j.types.ContractParameter.byteArray;
import static io.neow3j.types.ContractParameter.hash160;
import static io.neow3j.types.ContractParameter.integer;
import static io.neow3j.types.ContractParameter.string;
import static io.neow3j.types.StackItemType.MAP;
import static io.neow3j.utils.Numeric.hexToString;
import static io.neow3j.utils.Numeric.toHexString;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
/**
* Represents the NameService contract and provides methods to invoke its functions.
*/
public class NeoNameService extends NonFungibleToken {
public static final String NAME = "NameService";
private static final String ADD_ROOT = "addRoot";
private static final String SET_PRICE = "setPrice";
private static final String GET_PRICE = "getPrice";
private static final String IS_AVAILABLE = "isAvailable";
private static final String REGISTER = "register";
private static final String RENEW = "renew";
private static final String SET_ADMIN = "setAdmin";
private static final String SET_RECORD = "setRecord";
private static final String GET_RECORD = "getRecord";
private static final String DELETE_RECORD = "deleteRecord";
private static final String RESOLVE = "resolve";
private static final ByteStringStackItem NAME_PROPERTY =
new ByteStringStackItem("name".getBytes(UTF_8));
private static final ByteStringStackItem EXPI_PROPERTY =
new ByteStringStackItem("expiration".getBytes(UTF_8));
private static final String PROPERTIES = "properties";
private static final BigInteger MAXIMAL_PRICE = new BigInteger("1000000000000");
private static final Pattern ROOT_REGEX_PATTERN = Pattern.compile("^[a-z][a-z0-9]{0,15}$");
private static final Pattern NAME_REGEX_PATTERN = Pattern.compile(
"^(?=.{3,255}$)([a-z0-9]{1,62}\\.)+[a-z][a-z0-9]{0,15}$");
private static final Pattern IPV4_REGEX_PATTERN = Pattern.compile(
"^(?=\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\\.?){4}$");
private static final Pattern IPV6_REGEX_PATTERN = Pattern.compile(
"(?:^)(([0-9a-f]{1,4}:){7,7}[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,7}:|([0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}|([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}|([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}|([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}|[0-9a-f]{1,4}:((:[0-9a-f]{1,4}){1,6})|:((:[0-9a-f]{1,4}){1,7}|:))(?=$)");
/**
* Constructs a new {@code NeoNameService} contract that uses the given {@link Neow3j} instance
* for invocations.
*
* @param scriptHash The script hash of the name service contract.
* @param neow The {@link Neow3j} instance to use for invocations.
*/
public NeoNameService(Hash160 scriptHash, Neow3j neow) {
super(scriptHash, neow);
}
/**
* Returns the name of the NeoNameService contract. Doesn't require a call to the Neo node.
*
* @return the name.
*/
@Override
public String getName() {
return NAME;
}
/**
* Creates a transaction script to add a root domain and initializes a
* {@link TransactionBuilder} based on this script.
*
* Only committee members are allowed to add a new root domain.
*
* @param root The new root domain.
* @return a transaction builder.
*/
public TransactionBuilder addRoot(String root) {
checkRegexMatch(ROOT_REGEX_PATTERN, root);
return invokeFunction(ADD_ROOT, string(root));
}
/**
* Creates a transaction script to set the price for registering a domain and initializes a
* {@link TransactionBuilder} based on this script.
*
* Only committee members are allowed to set the price.
*
* @param price The price for registering a domain.
* @return a transaction builder.
*/
public TransactionBuilder setPrice(BigInteger price) {
if (!isValidPrice(price)) {
throw new IllegalArgumentException("The price needs to be greater than 0 and smaller " +
"than 1_000_000_000_000.");
}
return invokeFunction(SET_PRICE, integer(price));
}
// true if the price is in the allowed range, false otherwise.
private boolean isValidPrice(BigInteger price) {
return price.compareTo(BigInteger.ZERO) > 0 &&
price.compareTo(MAXIMAL_PRICE) <= 0;
}
/**
* Gets the price to register a domain.
*
* @return the price to register a domain.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public BigInteger getPrice() throws IOException {
return callFuncReturningInt(GET_PRICE);
}
/**
* Checks if the specified second-level domain name is available.
*
* @param name The domain name.
* @return true if the domain name is available, false otherwise.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public boolean isAvailable(String name) throws IOException {
checkDomainNameValidity(name);
try {
return callFuncReturningBool(IS_AVAILABLE, string(name));
} catch (IndexOutOfBoundsException e) {
String root = name.split("\\.")[1];
throw new IllegalArgumentException("The root domain '" + root + "' does not exist.");
}
}
/**
* Creates a transaction script to register a new domain and initializes a
* {@link TransactionBuilder} based on this script.
*
* @param name The domain name.
* @param owner The address of the domain owner.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder register(String name, Hash160 owner) throws IOException {
checkDomainNameAvailability(name, true);
return invokeFunction(REGISTER, string(name), hash160(owner));
}
// checks if a domain name is available
private void checkDomainNameAvailability(String name, boolean isForRegistration)
throws IOException {
boolean isAvailable = isAvailable(name);
if (isForRegistration && !isAvailable) {
throw new IllegalArgumentException("The domain name '" + name + "' is already taken.");
}
if (!isForRegistration && isAvailable) {
throw new IllegalArgumentException("The domain name '" + name + "' is not registered.");
}
}
// checks that the domain name matches the required regex and contains two levels.
private void checkDomainNameValidity(String name) {
checkRegexMatch(NAME_REGEX_PATTERN, name);
if (name.split("\\.").length != 2) {
throw new IllegalArgumentException("Only second-level domain names are allowed to be " +
"registered.");
}
}
// checks if an input matches the provided regex pattern.
private void checkRegexMatch(Pattern pattern, String input) {
if (!pattern.matcher(input).matches()) {
throw new IllegalArgumentException("The provided input does not match the required " +
"regex.");
}
}
/**
* Creates a transaction script to update the TTL of the domain name and initializes a
* {@link TransactionBuilder} based on this script.
*
* Each call will extend the validity period of the domain name by one year.
*
* Only supports renewing the second-level domain name.
*
* @param name The domain name.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder renew(String name) throws IOException {
checkDomainNameAvailability(name, false);
return invokeFunction(RENEW, string(name));
}
/**
* Creates a transaction script to set the admin for the specified domain name and
* initializes a {@link TransactionBuilder} based on this script.
*
* Requires to be signed by the current owner and the new admin of the domain.
*
* @param name The domain name.
* @param admin The script hash of the admin address.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder setAdmin(String name, Hash160 admin) throws IOException {
checkDomainNameAvailability(name, false);
return invokeFunction(SET_ADMIN, string(name), hash160(admin));
}
/**
* Creates a transaction script to set the type of the specified domain name and the
* corresponding type data and initializes a {@link TransactionBuilder} based on this script.
*
* @param name The domain name.
* @param type The record type.
* @param data The corresponding data.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder setRecord(String name, RecordType type, String data)
throws IOException {
checkDomainNameAvailability(name, false);
checkDataMatchingRecordType(type, data);
return invokeFunction(SET_RECORD, string(name), integer(type.byteValue()), string(data));
}
private void checkDataMatchingRecordType(RecordType type, String data) {
if (type.equals(RecordType.A)) {
checkRegexMatch(IPV4_REGEX_PATTERN, data);
} else if (type.equals(RecordType.CNAME)) {
checkRegexMatch(NAME_REGEX_PATTERN, data);
} else if (type.equals(RecordType.TXT)) {
byte[] bytes = data.getBytes(UTF_8);
if (bytes.length > 255) {
throw new IllegalArgumentException("The provided data is not valid for the record" +
" type TXT.");
}
} else {
checkRegexMatch(IPV6_REGEX_PATTERN, data);
}
}
/**
* Gets the type data of the domain.
*
* @param name The domain name.
* @param type The record type.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public String getRecord(String name, RecordType type) throws IOException {
checkDomainNameAvailability(name, false);
try {
return callFuncReturningString(GET_RECORD, string(name), integer(type.byteValue()));
} catch (UnexpectedReturnTypeException e) {
throw new IllegalArgumentException("No record of type " + type.jsonValue() + " found " +
"for the domain name '" + name + "'.");
}
}
/**
* Creates a transaction script to delete the record data initializes a
* {@link TransactionBuilder} based on this script.
*
* @param name The domain name.
* @param type The record type.
* @return a transaction builder.
*/
public TransactionBuilder deleteRecord(String name, RecordType type) {
return invokeFunction(DELETE_RECORD, string(name), integer(type.byteValue()));
}
/**
* Resolves a domain name.
*
* @param name The domain name.
* @param type The record type.
* @return the resolution result.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public String resolve(String name, RecordType type) throws IOException {
checkDomainNameAvailability(name, false);
try {
return callFuncReturningString(RESOLVE, string(name), integer(type.byteValue()));
} catch (UnexpectedReturnTypeException e) {
throw new IllegalArgumentException("No record of type " + type.jsonValue() + " found " +
"for the domain name '" + name + "'.");
}
}
/**
* Gets the owner of the domain name.
*
* @param name The domain name.
* @return the owner of the domain name.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public Hash160 ownerOf(String name) throws IOException {
checkDomainNameAvailability(name, false);
return ownerOf(name.getBytes(UTF_8));
}
/**
* Gets the properties of the domain name.
*
* @param name The domain name.
* @return the properties of the domain name as {@link NameState}.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public NameState properties(String name) throws IOException {
return properties(name.getBytes(UTF_8));
}
/**
* Gets the properties of the domain name.
*
* @param name The domain name.
* @return the properties of the domain name as {@link NameState}.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
@Override
public NameState properties(byte[] name) throws IOException {
String domainAsString = hexToString(toHexString(name));
checkDomainNameAvailability(domainAsString, false);
InvocationResult invocationResult =
callInvokeFunction(PROPERTIES, singletonList(byteArray(name)))
.getInvocationResult();
return deserializeProperties(invocationResult);
}
private NameState deserializeProperties(InvocationResult invocationResult) {
StackItem stackItem = invocationResult.getStack().get(0);
if (!stackItem.getType().equals(MAP)) {
throw new UnexpectedReturnTypeException(stackItem.getType(), MAP);
}
Map map = stackItem.getMap();
String name = map.get(NAME_PROPERTY).getString();
BigInteger expiration = map.get(EXPI_PROPERTY).getInteger();
return new NameState(name, expiration.longValue());
}
/**
* Creates a transaction script to transfer a domain name and initializes a
* {@link TransactionBuilder} based on this script.
*
* The returned {@link TransactionBuilder} is ready to be signed and sent. The {@code from}
* account is set as a signer on the transaction.
*
* @param from The owner of the domain name.
* @param to The receiver of the domain name.
* @param name The domain name.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder transfer(Account from, Hash160 to, String name)
throws IOException {
return transfer(from, to, name, null);
}
/**
* Creates a transaction script to transfer a domain name and initializes a
* {@link TransactionBuilder} based on this script.
*
* The returned {@link TransactionBuilder} is ready to be signed and sent. The {@code from}
* account is set as a signer on the transaction.
*
* @param from The owner of the domain name.
* @param to The receiver of the domain name.
* @param name The domain name.
* @param data The data that is passed to the {@code onNEP11Payment} method of the receiving
* smart contract.
* @return a transaction builder.
* @throws IOException if there was a problem fetching information from the Neo node.
*/
public TransactionBuilder transfer(Account from, Hash160 to, String name,
ContractParameter data) throws IOException {
checkDomainNameAvailability(name, false);
return transfer(from, to, name.getBytes(UTF_8), data);
}
}