Skip to content

Commit a8cdba7

Browse files
committed
feat: add missing crypto sdk methods
1 parent e1394ce commit a8cdba7

9 files changed

Lines changed: 188 additions & 12 deletions

File tree

src/main/java/org/arkecosystem/crypto/identities/Address.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,8 @@ public static String fromPrivateKey(ECKey privateKey) {
4040
byte[] publicKeyBytes = privateKey.getPubKey();
4141
return fromPublicKey(Hex.encode(publicKeyBytes));
4242
}
43+
44+
public static boolean validate(String address) {
45+
return address != null && address.matches("^0x[a-fA-F0-9]{40}$");
46+
}
4347
}

src/main/java/org/arkecosystem/crypto/identities/PrivateKey.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package org.arkecosystem.crypto.identities;
22

3+
import java.util.Arrays;
4+
import org.arkecosystem.crypto.configuration.Network;
5+
import org.arkecosystem.crypto.encoding.Base58;
36
import org.arkecosystem.crypto.encoding.Hex;
47
import org.bitcoinj.core.ECKey;
58
import org.bitcoinj.core.Sha256Hash;
@@ -14,4 +17,22 @@ public static ECKey fromPassphrase(String passphrase) {
1417
public static ECKey fromHex(String privateKey) {
1518
return ECKey.fromPrivate(Hex.decode(privateKey), true);
1619
}
20+
21+
public static ECKey fromWif(String wif) {
22+
byte[] decoded = Base58.decodeChecked(wif);
23+
24+
if (decoded.length < 33) {
25+
throw new IllegalArgumentException("Invalid WIF: payload too short.");
26+
}
27+
28+
int expectedVersion = Network.get().wif() & 0xff;
29+
if ((decoded[0] & 0xff) != expectedVersion) {
30+
throw new IllegalArgumentException(
31+
"Invalid WIF: version byte does not match the active network.");
32+
}
33+
34+
byte[] privateKeyBytes = Arrays.copyOfRange(decoded, 1, 33);
35+
36+
return ECKey.fromPrivate(privateKeyBytes, true);
37+
}
1738
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package org.arkecosystem.crypto.identities;
22

3+
import org.arkecosystem.crypto.encoding.Hex;
4+
import org.bitcoinj.core.ECKey;
5+
36
public class PublicKey {
47
public static String fromPassphrase(String passphrase) {
58
return PrivateKey.fromPassphrase(passphrase).getPublicKeyAsHex();
69
}
10+
11+
public static ECKey fromHex(String publicKey) {
12+
ECKey key = ECKey.fromPublicOnly(Hex.decode(publicKey));
13+
return ECKey.fromPublicOnly(key.getPubKeyPoint().getEncoded(true));
14+
}
715
}

src/main/java/org/arkecosystem/crypto/transactions/builder/AbstractTransactionBuilder.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ public TBuilder sign(String passphrase) {
5555
return this.instance();
5656
}
5757

58+
public TBuilder legacySecondSign(String passphrase, String secondPassphrase) {
59+
this.transaction.sign(passphrase);
60+
this.transaction.legacySecondSign(secondPassphrase);
61+
this.transaction.computeId();
62+
return this.instance();
63+
}
64+
5865
public boolean verify() {
5966
return this.transaction.verify();
6067
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public abstract class AbstractTransaction {
3333
public List<String> multipaymentRecipients;
3434
public List<BigInteger> multipaymentAmounts;
3535
public String username;
36+
public String legacySecondSignature;
3637

3738
public AbstractTransaction() {}
3839

@@ -105,11 +106,23 @@ public byte[] hash(boolean skipSignature) {
105106
}
106107

107108
public AbstractTransaction sign(String passphrase) {
108-
byte[] hash = this.hash(true);
109-
110109
ECKey privateKey = PrivateKey.fromPassphrase(passphrase);
111110
this.senderPublicKey = privateKey.getPublicKeyAsHex();
112111

112+
this.signature = signHash(this.hash(true), privateKey);
113+
114+
return this;
115+
}
116+
117+
public AbstractTransaction legacySecondSign(String secondPassphrase) {
118+
ECKey privateKey = PrivateKey.fromPassphrase(secondPassphrase);
119+
120+
this.legacySecondSignature = signHash(this.hash(true), privateKey);
121+
122+
return this;
123+
}
124+
125+
private static String signHash(byte[] hash, ECKey privateKey) {
113126
ECKey.ECDSASignature signature = privateKey.sign(Sha256Hash.wrap(hash));
114127

115128
int recId = -1;
@@ -135,9 +148,7 @@ public AbstractTransaction sign(String passphrase) {
135148
System.arraycopy(signatureBytes, 0, signatureWithRecId, 0, 64);
136149
signatureWithRecId[64] = (byte) recId;
137150

138-
this.signature = Hex.encode(signatureWithRecId);
139-
140-
return this;
151+
return Hex.encode(signatureWithRecId);
141152
}
142153

143154
private static byte[] bigIntegerToBytes(BigInteger b, int numBytes) {

src/test/java/org/arkecosystem/crypto/identities/AddressTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.arkecosystem.crypto.identities;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
46

57
import org.bitcoinj.core.ECKey;
68
import org.junit.jupiter.api.Test;
@@ -27,4 +29,35 @@ public void fromPrivateKey() {
2729
String actual = Address.fromPrivateKey(privateKey);
2830
assertEquals("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01", actual);
2931
}
32+
33+
@Test
34+
public void validate_accepts_valid_checksum_address() {
35+
assertTrue(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B01"));
36+
}
37+
38+
@Test
39+
public void validate_accepts_lowercase_address() {
40+
assertTrue(Address.validate("0xb0ff9213f7226bbb72b84de16af86e56f1f38b01"));
41+
}
42+
43+
@Test
44+
public void validate_rejects_missing_prefix() {
45+
assertFalse(Address.validate("b0FF9213f7226bBB72b84dE16af86e56f1f38B01"));
46+
}
47+
48+
@Test
49+
public void validate_rejects_wrong_length() {
50+
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B0"));
51+
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38B011"));
52+
}
53+
54+
@Test
55+
public void validate_rejects_non_hex_characters() {
56+
assertFalse(Address.validate("0xb0FF9213f7226bBB72b84dE16af86e56f1f38BZZ"));
57+
}
58+
59+
@Test
60+
public void validate_rejects_null() {
61+
assertFalse(Address.validate(null));
62+
}
3063
}

src/test/java/org/arkecosystem/crypto/identities/PrivateKeyTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
package org.arkecosystem.crypto.identities;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertThrows;
45

6+
import java.io.IOException;
7+
import org.arkecosystem.crypto.configuration.Network;
8+
import org.arkecosystem.crypto.networks.Devnet;
9+
import org.arkecosystem.crypto.networks.Mainnet;
10+
import org.junit.jupiter.api.AfterEach;
11+
import org.junit.jupiter.api.BeforeEach;
512
import org.junit.jupiter.api.Test;
613

714
public class PrivateKeyTest {
815

16+
@BeforeEach
17+
void setUp() {
18+
Network.set(new Devnet());
19+
}
20+
21+
@AfterEach
22+
void tearDown() {
23+
Network.set(new Devnet());
24+
}
25+
926
@Test
1027
public void fromPassphrase() {
1128
String actual =
@@ -21,4 +38,35 @@ public void fromHex() {
2138
.getPrivateKeyAsHex();
2239
assertEquals("d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", actual);
2340
}
41+
42+
@Test
43+
public void fromWif_round_trips_with_fromPassphrase() throws IOException {
44+
String wif = WIF.fromPassphrase("this is a top secret passphrase");
45+
46+
String fromWif = PrivateKey.fromWif(wif).getPrivateKeyAsHex();
47+
String fromPassphrase =
48+
PrivateKey.fromPassphrase("this is a top secret passphrase").getPrivateKeyAsHex();
49+
50+
assertEquals(fromPassphrase, fromWif);
51+
}
52+
53+
@Test
54+
public void fromWif_rejects_when_version_byte_belongs_to_another_network() throws IOException {
55+
String mainnetWif;
56+
try {
57+
Network.set(new Mainnet());
58+
mainnetWif = WIF.fromPassphrase("this is a top secret passphrase");
59+
} finally {
60+
Network.set(new Devnet());
61+
}
62+
63+
// Mainnet and Devnet share the same wif byte (170) so this branch only
64+
// triggers when the prefix differs, e.g. against testnet (186).
65+
Network.set(new org.arkecosystem.crypto.networks.Testnet());
66+
try {
67+
assertThrows(IllegalArgumentException.class, () -> PrivateKey.fromWif(mainnetWif));
68+
} finally {
69+
Network.set(new Devnet());
70+
}
71+
}
2472
}

src/test/java/org/arkecosystem/crypto/identities/PublicKeyTest.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,23 @@ public void fromPassphrase() {
1212
}
1313

1414
@Test
15-
public void fromHex() {
16-
String actual =
17-
PrivateKey.fromHex(
18-
"d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712")
19-
.getPrivateKeyAsHex();
20-
Assertions.assertEquals(
21-
"d8839c2432bfd0a67ef10a804ba991eabba19f154a3d707917681d45822a5712", actual);
15+
public void fromHex_round_trips_to_same_compressed_hex() {
16+
String hex = "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192";
17+
18+
String actual = PublicKey.fromHex(hex).getPublicKeyAsHex();
19+
20+
Assertions.assertEquals(hex, actual);
21+
}
22+
23+
@Test
24+
public void fromHex_accepts_uncompressed_public_key() {
25+
String compressed = "034151a3ec46b5670a682b0a63394f863587d1bc97483b1b6c70eb58e7f0aed192";
26+
String uncompressed =
27+
org.arkecosystem.crypto.encoding.Hex.encode(
28+
PublicKey.fromHex(compressed).getPubKeyPoint().getEncoded(false));
29+
30+
String actual = PublicKey.fromHex(uncompressed).getPublicKeyAsHex();
31+
32+
Assertions.assertEquals(compressed, actual);
2233
}
2334
}

src/test/java/org/arkecosystem/crypto/transactions/builder/TransferBuilderTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,37 @@ public void it_should_sign_it_with_a_passphrase() throws Exception {
3232
assertEquals(data.get("id"), builder.transaction.getId());
3333
assertTrue(builder.verify());
3434
}
35+
36+
@Test
37+
public void it_should_attach_a_legacy_second_signature() {
38+
TransferBuilder builder =
39+
new TransferBuilder()
40+
.gasPrice(5_000_000_000L)
41+
.gasLimit(21000)
42+
.nonce(1L)
43+
.recipientAddress("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A")
44+
.value("100000000")
45+
.legacySecondSign(this.passphrase, "second secret passphrase");
46+
47+
assertNotNull(builder.transaction.signature);
48+
assertNotNull(builder.transaction.legacySecondSignature);
49+
assertEquals(130, builder.transaction.legacySecondSignature.length());
50+
assertNotEquals(builder.transaction.signature, builder.transaction.legacySecondSignature);
51+
assertTrue(builder.verify());
52+
}
53+
54+
@Test
55+
public void legacy_second_signature_is_not_set_by_a_regular_sign() {
56+
TransferBuilder builder =
57+
new TransferBuilder()
58+
.gasPrice(5_000_000_000L)
59+
.gasLimit(21000)
60+
.nonce(1L)
61+
.recipientAddress("0xb693449AdDa7EFc015D87944EAE8b7C37EB1690A")
62+
.value("100000000")
63+
.sign(this.passphrase);
64+
65+
assertNotNull(builder.transaction.signature);
66+
assertNull(builder.transaction.legacySecondSignature);
67+
}
3568
}

0 commit comments

Comments
 (0)