Skip to content

Commit 127f9fe

Browse files
committed
feat: add username registration builder
1 parent 65b212f commit 127f9fe

6 files changed

Lines changed: 248 additions & 0 deletions

File tree

‎src/main/java/org/arkecosystem/crypto/enums/AbiFunction.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ public enum AbiFunction {
55
UNVOTE("unvote"),
66
VALIDATOR_REGISTRATION("registerValidator"),
77
VALIDATOR_RESIGNATION("resignValidator"),
8+
USERNAME_REGISTRATION("registerUsername"),
9+
USERNAME_RESIGNATION("resignUsername"),
810
MULTIPAYMENT("pay"),
911
TRANSFER("transfer"),
1012
APPROVE("approve");

‎src/main/java/org/arkecosystem/crypto/transactions/Deserializer.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ private AbstractTransaction guessTransactionFromTransactionData(
106106
return new ValidatorRegistration(transactionData.toHashMap());
107107
} else if (TransactionTypeIdentifier.isValidatorResignation(payload)) {
108108
return new ValidatorResignation(transactionData.toHashMap());
109+
} else if (TransactionTypeIdentifier.isUsernameRegistration(payload)) {
110+
return new UsernameRegistration(transactionData.toHashMap());
109111
}
110112

111113
return new EvmCall();
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package org.arkecosystem.crypto.transactions.builder;
2+
3+
import org.arkecosystem.crypto.enums.ContractAddresses;
4+
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
5+
import org.arkecosystem.crypto.transactions.types.UsernameRegistration;
6+
7+
public class UsernameRegistrationBuilder
8+
extends AbstractTransactionBuilder<UsernameRegistrationBuilder> {
9+
10+
public UsernameRegistrationBuilder() {
11+
super();
12+
this.transaction.recipientAddress = ContractAddresses.USERNAMES.address();
13+
}
14+
15+
public UsernameRegistrationBuilder username(String username) {
16+
validateUsername(username);
17+
18+
this.transaction.username = username;
19+
this.transaction.refreshPayloadData();
20+
21+
return this.instance();
22+
}
23+
24+
private static void validateUsername(String username) {
25+
if (username == null || username.isEmpty() || username.length() > 20) {
26+
throw new IllegalArgumentException(
27+
"Username must be between 1 and 20 characters long. Got "
28+
+ (username == null ? 0 : username.length())
29+
+ " characters.");
30+
}
31+
32+
if (!username.matches("^[a-z0-9_]+$")) {
33+
throw new IllegalArgumentException(
34+
"Username can only contain lowercase letters, numbers and underscores.");
35+
}
36+
37+
if (username.startsWith("_") || username.endsWith("_")) {
38+
throw new IllegalArgumentException("Username cannot start or end with an underscore.");
39+
}
40+
41+
if (username.contains("__")) {
42+
throw new IllegalArgumentException("Username cannot contain consecutive underscores.");
43+
}
44+
}
45+
46+
@Override
47+
protected AbstractTransaction getTransactionInstance() {
48+
return new UsernameRegistration();
49+
}
50+
51+
@Override
52+
protected UsernameRegistrationBuilder instance() {
53+
return this;
54+
}
55+
}

‎src/main/java/org/arkecosystem/crypto/transactions/types/AbstractTransaction.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public abstract class AbstractTransaction {
3232
public String vote;
3333
public List<String> multipaymentRecipients;
3434
public List<BigInteger> multipaymentAmounts;
35+
public String username;
3536

3637
public AbstractTransaction() {}
3738

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.arkecosystem.crypto.transactions.types;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.Map;
6+
import org.arkecosystem.crypto.enums.AbiFunction;
7+
import org.arkecosystem.crypto.enums.ContractAbiType;
8+
import org.arkecosystem.crypto.utils.AbiDecoder;
9+
import org.arkecosystem.crypto.utils.AbiEncoder;
10+
11+
public class UsernameRegistration extends AbstractTransaction {
12+
13+
public UsernameRegistration() {
14+
super();
15+
}
16+
17+
public UsernameRegistration(Map<String, Object> data) {
18+
super(data);
19+
20+
List<Object> payload = decodeUsernamePayload(data);
21+
if (payload != null && !payload.isEmpty()) {
22+
this.username = payload.get(0).toString();
23+
}
24+
}
25+
26+
@Override
27+
public String getPayload() {
28+
if (this.username == null || this.username.isEmpty()) {
29+
return "";
30+
}
31+
32+
try {
33+
return new AbiEncoder(ContractAbiType.USERNAMES)
34+
.encodeFunctionCall(
35+
AbiFunction.USERNAME_REGISTRATION.toString(),
36+
Collections.singletonList(this.username));
37+
} catch (Exception e) {
38+
throw new RuntimeException("Error encoding username registration call", e);
39+
}
40+
}
41+
42+
private static List<Object> decodeUsernamePayload(Map<String, Object> data) {
43+
if (data == null || !data.containsKey("data")) return null;
44+
45+
String payload = (String) data.get("data");
46+
if (payload == null || payload.isEmpty()) return null;
47+
48+
try {
49+
AbiDecoder decoder = new AbiDecoder(ContractAbiType.USERNAMES);
50+
Map<String, Object> decoded = decoder.decodeFunctionData(payload);
51+
return (List<Object>) decoded.get("args");
52+
} catch (Exception e) {
53+
return null;
54+
}
55+
}
56+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package org.arkecosystem.crypto.transactions.builder;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.arkecosystem.crypto.AbstractTest;
6+
import org.arkecosystem.crypto.encoding.Hex;
7+
import org.arkecosystem.crypto.enums.ContractAddresses;
8+
import org.arkecosystem.crypto.transactions.Deserializer;
9+
import org.arkecosystem.crypto.transactions.types.AbstractTransaction;
10+
import org.arkecosystem.crypto.transactions.types.UsernameRegistration;
11+
import org.junit.jupiter.api.Test;
12+
13+
public class UsernameRegistrationBuilderTest extends AbstractTest {
14+
15+
private static final String REGISTER_SELECTOR = "0x36a94134";
16+
17+
@Test
18+
public void it_should_default_to_the_usernames_well_known_contract() {
19+
UsernameRegistrationBuilder builder = new UsernameRegistrationBuilder();
20+
21+
assertEquals(ContractAddresses.USERNAMES.address(), builder.transaction.recipientAddress);
22+
}
23+
24+
@Test
25+
public void it_should_encode_register_username_selector_and_argument() {
26+
UsernameRegistrationBuilder builder =
27+
new UsernameRegistrationBuilder()
28+
.gasPrice(5_000_000_000L)
29+
.gasLimit(200_000)
30+
.nonce(1L)
31+
.username("alice");
32+
33+
assertEquals("alice", builder.transaction.username);
34+
assertTrue(
35+
builder.transaction.data.startsWith(REGISTER_SELECTOR),
36+
"expected payload to start with register selector but was "
37+
+ builder.transaction.data);
38+
}
39+
40+
@Test
41+
public void it_should_sign_and_verify_a_username_registration_transaction() {
42+
UsernameRegistrationBuilder builder =
43+
new UsernameRegistrationBuilder()
44+
.gasPrice(5_000_000_000L)
45+
.gasLimit(200_000)
46+
.nonce(1L)
47+
.username("alice")
48+
.sign(this.passphrase);
49+
50+
assertNotNull(builder.transaction.signature);
51+
assertNotNull(builder.transaction.id);
52+
assertTrue(builder.verify());
53+
}
54+
55+
@Test
56+
public void it_should_round_trip_through_serialization() {
57+
UsernameRegistrationBuilder builder =
58+
new UsernameRegistrationBuilder()
59+
.gasPrice(5_000_000_000L)
60+
.gasLimit(200_000)
61+
.nonce(7L)
62+
.username("bob_42")
63+
.sign(this.passphrase);
64+
65+
String serialized = Hex.encode(builder.transaction.serialize());
66+
AbstractTransaction restored = Deserializer.newDeserializer(serialized).deserialize();
67+
68+
assertInstanceOf(UsernameRegistration.class, restored);
69+
UsernameRegistration registration = (UsernameRegistration) restored;
70+
assertEquals(builder.transaction.id, registration.id);
71+
assertEquals("bob_42", registration.username);
72+
assertEquals(
73+
builder.transaction.recipientAddress.toLowerCase(),
74+
registration.recipientAddress.toLowerCase());
75+
}
76+
77+
@Test
78+
public void it_should_produce_empty_payload_when_no_username_is_set() {
79+
UsernameRegistrationBuilder builder = new UsernameRegistrationBuilder();
80+
81+
assertEquals("", builder.transaction.data);
82+
}
83+
84+
@Test
85+
public void it_should_reject_empty_username() {
86+
IllegalArgumentException thrown =
87+
assertThrows(
88+
IllegalArgumentException.class,
89+
() -> new UsernameRegistrationBuilder().username(""));
90+
assertTrue(thrown.getMessage().contains("between 1 and 20"));
91+
}
92+
93+
@Test
94+
public void it_should_reject_username_longer_than_20_characters() {
95+
assertThrows(
96+
IllegalArgumentException.class,
97+
() -> new UsernameRegistrationBuilder().username("a_very_long_username_xx"));
98+
}
99+
100+
@Test
101+
public void it_should_reject_uppercase_letters() {
102+
IllegalArgumentException thrown =
103+
assertThrows(
104+
IllegalArgumentException.class,
105+
() -> new UsernameRegistrationBuilder().username("Alice"));
106+
assertTrue(thrown.getMessage().contains("lowercase"));
107+
}
108+
109+
@Test
110+
public void it_should_reject_special_characters() {
111+
assertThrows(
112+
IllegalArgumentException.class,
113+
() -> new UsernameRegistrationBuilder().username("alice!"));
114+
}
115+
116+
@Test
117+
public void it_should_reject_leading_or_trailing_underscore() {
118+
assertThrows(
119+
IllegalArgumentException.class,
120+
() -> new UsernameRegistrationBuilder().username("_alice"));
121+
assertThrows(
122+
IllegalArgumentException.class,
123+
() -> new UsernameRegistrationBuilder().username("alice_"));
124+
}
125+
126+
@Test
127+
public void it_should_reject_consecutive_underscores() {
128+
assertThrows(
129+
IllegalArgumentException.class,
130+
() -> new UsernameRegistrationBuilder().username("al__ice"));
131+
}
132+
}

0 commit comments

Comments
 (0)