Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ public enum AbiFunction {
UNVOTE("unvote"),
VALIDATOR_REGISTRATION("registerValidator"),
VALIDATOR_RESIGNATION("resignValidator"),
USERNAME_REGISTRATION("registerUsername"),
USERNAME_RESIGNATION("resignUsername"),
MULTIPAYMENT("pay"),
TRANSFER("transfer"),
APPROVE("approve");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ private AbstractTransaction guessTransactionFromTransactionData(
return new ValidatorRegistration(transactionData.toHashMap());
} else if (TransactionTypeIdentifier.isValidatorResignation(payload)) {
return new ValidatorResignation(transactionData.toHashMap());
} else if (TransactionTypeIdentifier.isUsernameRegistration(payload)) {
return new UsernameRegistration(transactionData.toHashMap());
}

return new EvmCall();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.arkecosystem.crypto.transactions.builder;

import org.arkecosystem.crypto.enums.ContractAddresses;
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
import org.arkecosystem.crypto.transactions.types.UsernameRegistration;

public class UsernameRegistrationBuilder
extends AbstractTransactionBuilder<UsernameRegistrationBuilder> {

public UsernameRegistrationBuilder() {
super();
this.transaction.recipientAddress = ContractAddresses.USERNAMES.address();
}

public UsernameRegistrationBuilder username(String username) {
validateUsername(username);

this.transaction.username = username;
this.transaction.refreshPayloadData();

return this.instance();
}

private static void validateUsername(String username) {
if (username == null || username.isEmpty() || username.length() > 20) {
throw new IllegalArgumentException(
"Username must be between 1 and 20 characters long. Got "
+ (username == null ? 0 : username.length())
+ " characters.");
}

if (!username.matches("^[a-z0-9_]+$")) {
throw new IllegalArgumentException(
"Username can only contain lowercase letters, numbers and underscores.");
}

if (username.startsWith("_") || username.endsWith("_")) {
throw new IllegalArgumentException("Username cannot start or end with an underscore.");
}

if (username.contains("__")) {
throw new IllegalArgumentException("Username cannot contain consecutive underscores.");
}
}

@Override
protected AbstractTransaction getTransactionInstance() {
return new UsernameRegistration();
}

@Override
protected UsernameRegistrationBuilder instance() {
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public abstract class AbstractTransaction {
public String vote;
public List<String> multipaymentRecipients;
public List<BigInteger> multipaymentAmounts;
public String username;

public AbstractTransaction() {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.arkecosystem.crypto.transactions.types;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.arkecosystem.crypto.enums.AbiFunction;
import org.arkecosystem.crypto.enums.ContractAbiType;
import org.arkecosystem.crypto.utils.AbiDecoder;
import org.arkecosystem.crypto.utils.AbiEncoder;

public class UsernameRegistration extends AbstractTransaction {

public UsernameRegistration() {
super();
}

public UsernameRegistration(Map<String, Object> data) {
super(data);

List<Object> payload = decodeUsernamePayload(data);
if (payload != null && !payload.isEmpty()) {
this.username = payload.get(0).toString();
}
}

@Override
public String getPayload() {
if (this.username == null || this.username.isEmpty()) {
return "";
}

try {
return new AbiEncoder(ContractAbiType.USERNAMES)
.encodeFunctionCall(
AbiFunction.USERNAME_REGISTRATION.toString(),
Collections.singletonList(this.username));
} catch (Exception e) {
throw new RuntimeException("Error encoding username registration call", e);
}
}

private static List<Object> decodeUsernamePayload(Map<String, Object> data) {
if (data == null || !data.containsKey("data")) return null;

String payload = (String) data.get("data");
if (payload == null || payload.isEmpty()) return null;

try {
AbiDecoder decoder = new AbiDecoder(ContractAbiType.USERNAMES);
Map<String, Object> decoded = decoder.decodeFunctionData(payload);
return (List<Object>) decoded.get("args");
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package org.arkecosystem.crypto.transactions.builder;

import static org.junit.jupiter.api.Assertions.*;

import org.arkecosystem.crypto.AbstractTest;
import org.arkecosystem.crypto.encoding.Hex;
import org.arkecosystem.crypto.enums.ContractAddresses;
import org.arkecosystem.crypto.transactions.Deserializer;
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
import org.arkecosystem.crypto.transactions.types.UsernameRegistration;
import org.junit.jupiter.api.Test;

public class UsernameRegistrationBuilderTest extends AbstractTest {

private static final String REGISTER_SELECTOR = "0x36a94134";

@Test
public void it_should_default_to_the_usernames_well_known_contract() {
UsernameRegistrationBuilder builder = new UsernameRegistrationBuilder();

assertEquals(ContractAddresses.USERNAMES.address(), builder.transaction.recipientAddress);
}

@Test
public void it_should_encode_register_username_selector_and_argument() {
UsernameRegistrationBuilder builder =
new UsernameRegistrationBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(200_000)
.nonce(1L)
.username("alice");

assertEquals("alice", builder.transaction.username);
assertTrue(
builder.transaction.data.startsWith(REGISTER_SELECTOR),
"expected payload to start with register selector but was "
+ builder.transaction.data);
}

@Test
public void it_should_sign_and_verify_a_username_registration_transaction() {
UsernameRegistrationBuilder builder =
new UsernameRegistrationBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(200_000)
.nonce(1L)
.username("alice")
.sign(this.passphrase);

assertNotNull(builder.transaction.signature);
assertNotNull(builder.transaction.id);
assertTrue(builder.verify());
}

@Test
public void it_should_round_trip_through_serialization() {
UsernameRegistrationBuilder builder =
new UsernameRegistrationBuilder()
.gasPrice(5_000_000_000L)
.gasLimit(200_000)
.nonce(7L)
.username("bob_42")
.sign(this.passphrase);

String serialized = Hex.encode(builder.transaction.serialize());
AbstractTransaction restored = Deserializer.newDeserializer(serialized).deserialize();

assertInstanceOf(UsernameRegistration.class, restored);
UsernameRegistration registration = (UsernameRegistration) restored;
assertEquals(builder.transaction.id, registration.id);
assertEquals("bob_42", registration.username);
assertEquals(
builder.transaction.recipientAddress.toLowerCase(),
registration.recipientAddress.toLowerCase());
}

@Test
public void it_should_produce_empty_payload_when_no_username_is_set() {
UsernameRegistrationBuilder builder = new UsernameRegistrationBuilder();

assertEquals("", builder.transaction.data);
}

@Test
public void it_should_reject_empty_username() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username(""));
assertTrue(thrown.getMessage().contains("between 1 and 20"));
}

@Test
public void it_should_reject_username_longer_than_20_characters() {
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("a_very_long_username_xx"));
}

@Test
public void it_should_reject_uppercase_letters() {
IllegalArgumentException thrown =
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("Alice"));
assertTrue(thrown.getMessage().contains("lowercase"));
}

@Test
public void it_should_reject_special_characters() {
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("alice!"));
}

@Test
public void it_should_reject_leading_or_trailing_underscore() {
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("_alice"));
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("alice_"));
}

@Test
public void it_should_reject_consecutive_underscores() {
assertThrows(
IllegalArgumentException.class,
() -> new UsernameRegistrationBuilder().username("al__ice"));
}
}
Loading