Skip to content

Commit d4aef0f

Browse files
committed
ss58 address supported
Added `SS58Codec` Added `AccountIdEncoder` - RpcEncoder for AccountId to represent it as ss58 address Test for `system.accountNextIndex()` were modified - mocks don't need more Added `MetadataRegistry` - it's not a target solution, there is just hard-coded ss58 address format
1 parent 463ad3c commit d4aef0f

File tree

10 files changed

+237
-23
lines changed

10 files changed

+237
-23
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ allprojects {
1010
repositories {
1111
mavenLocal()
1212
mavenCentral()
13+
maven { url 'https://jitpack.io' }
1314
}
1415
}
1516

crypto/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
dependencies {
2+
implementation 'com.github.multiformats:java-multibase:1.1.0'
3+
implementation 'org.bouncycastle:bcprov-jdk15on:1.69'
24
implementation project(":common")
35
implementation project(":types")
46
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.strategyobject.substrateclient.crypto.ss58;
2+
3+
import lombok.EqualsAndHashCode;
4+
import lombok.Getter;
5+
import lombok.NonNull;
6+
import lombok.RequiredArgsConstructor;
7+
8+
@RequiredArgsConstructor(staticName = "from")
9+
@EqualsAndHashCode
10+
@Getter
11+
public class AddressWithPrefix {
12+
private final byte @NonNull [] address;
13+
private final short prefix;
14+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.strategyobject.substrateclient.crypto.ss58;
2+
3+
import com.google.common.base.Preconditions;
4+
import io.ipfs.multibase.Base58;
5+
import lombok.NonNull;
6+
import lombok.val;
7+
import org.bouncycastle.jcajce.provider.digest.Blake2b;
8+
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Arrays;
11+
12+
public final class SS58Codec {
13+
private static final byte[] PREFIX = "SS58PRE".getBytes(StandardCharsets.US_ASCII);
14+
private static final int CHECKSUM_LEN = 2;
15+
private static final int ADDRESS_LENGTH = 32;
16+
17+
private SS58Codec() {
18+
}
19+
20+
public static AddressWithPrefix decode(@NonNull String encoded) {
21+
Preconditions.checkArgument(encoded.length() > 0);
22+
23+
val data = Base58.decode(encoded);
24+
if (data.length < 2) {
25+
throw new IllegalArgumentException("Incorrect length of data.");
26+
}
27+
28+
int typeLen;
29+
short prefix;
30+
31+
if (data[0] >= 0 && data[0] <= 63) {
32+
typeLen = 1;
33+
prefix = data[0];
34+
} else if (data[0] >= 64) {
35+
byte lower = (byte) (Byte.toUnsignedInt((byte) (data[0] << 2)) | Byte.toUnsignedInt((byte) (data[1] >> 6)));
36+
byte upper = (byte) (data[1] & 0b00111111);
37+
38+
typeLen = 2;
39+
prefix = (short) (Byte.toUnsignedInt(lower) | (upper << 8));
40+
} else {
41+
throw new IllegalArgumentException("Unknown version.");
42+
}
43+
44+
if (data.length != typeLen + ADDRESS_LENGTH + CHECKSUM_LEN) {
45+
throw new IllegalArgumentException("Incorrect length of data.");
46+
}
47+
48+
val checkSumData = new byte[PREFIX.length + typeLen + ADDRESS_LENGTH];
49+
System.arraycopy(PREFIX, 0, checkSumData, 0, PREFIX.length);
50+
System.arraycopy(data, 0, checkSumData, PREFIX.length, typeLen + ADDRESS_LENGTH);
51+
val checksum = new Blake2b.Blake2b512().digest(checkSumData);
52+
if (checksum[0] != data[data.length - CHECKSUM_LEN] || checksum[1] != data[data.length - CHECKSUM_LEN + 1]) {
53+
throw new IllegalArgumentException("Incorrect checksum.");
54+
}
55+
56+
return AddressWithPrefix.from(Arrays.copyOfRange(data, typeLen, typeLen + ADDRESS_LENGTH), prefix);
57+
}
58+
59+
public static String encode(byte @NonNull [] address, short prefix) {
60+
Preconditions.checkArgument(address.length == ADDRESS_LENGTH,
61+
"The length of address must be 32, but was: " + address.length);
62+
63+
val ident = prefix & 0b0011_1111_1111_1111;
64+
Preconditions.checkArgument(ident == prefix,
65+
"The prefix size is restricted by 14 bits.");
66+
67+
byte[] data;
68+
int typeLen;
69+
70+
if (ident <= 63) {
71+
typeLen = 1;
72+
data = new byte[PREFIX.length + typeLen + address.length];
73+
data[PREFIX.length] = (byte) ident;
74+
} else {
75+
typeLen = 2;
76+
data = new byte[PREFIX.length + typeLen + address.length];
77+
val first = (ident & 0b0000_0000_1111_1100) >> 2;
78+
val second = (ident >> 8) | (ident & 0b0000_0000_0000_0011) << 6;
79+
data[PREFIX.length] = (byte) (first | 0b01000000);
80+
data[PREFIX.length + 1] = (byte) second;
81+
}
82+
83+
System.arraycopy(PREFIX, 0, data, 0, PREFIX.length);
84+
System.arraycopy(address, 0, data, PREFIX.length + typeLen, address.length);
85+
86+
val checksum = new Blake2b.Blake2b512().digest(data);
87+
val encodable = new byte[typeLen + address.length + CHECKSUM_LEN];
88+
System.arraycopy(data, PREFIX.length, encodable, 0, typeLen + address.length);
89+
System.arraycopy(checksum, 0, encodable, typeLen + address.length, CHECKSUM_LEN);
90+
91+
return Base58.encode(encodable);
92+
}
93+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.strategyobject.substrateclient.crypto.ss58;
2+
3+
import com.strategyobject.substrateclient.common.utils.HexConverter;
4+
import lombok.val;
5+
import org.junit.jupiter.params.ParameterizedTest;
6+
import org.junit.jupiter.params.provider.CsvSource;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertThrows;
10+
11+
public class SS58CodecTests {
12+
@ParameterizedTest
13+
@CsvSource(value = {
14+
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:42",
15+
"cEaNSpz4PxFcZ7nT1VEKrKewH67rfx6MfcM6yKojyyPz7qaqp:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:64",
16+
"yGHXkYLYqxijLKKfd9Q2CB9shRVu8rPNBS53wvwGTutYg4zTg:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:255",
17+
"VByeGLMtP8r8BYQpNX1sb2VtAW8GYCbtFAeXJwsA2ur3MNRdq:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:256",
18+
"yNa8JpqfFB3q8A29rCwSgxvdU94ufJw2yKKxDgznS5m1PoFvn:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16383",
19+
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48:42"
20+
},
21+
delimiterString = ":")
22+
void encode(String expected, String hex, short prefix) {
23+
val actual = SS58Codec.encode(HexConverter.toBytes(hex), prefix);
24+
25+
assertEquals(expected, actual);
26+
}
27+
28+
@ParameterizedTest
29+
@CsvSource(value = {
30+
"0xd4:42",
31+
"0x:42",
32+
"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16384"
33+
},
34+
delimiterString = ":")
35+
void encodeThrows(String hex, short prefix) {
36+
assertThrows(IllegalArgumentException.class, () -> SS58Codec.encode(HexConverter.toBytes(hex), prefix));
37+
}
38+
39+
@ParameterizedTest
40+
@CsvSource(value = {
41+
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:42",
42+
"cEaNSpz4PxFcZ7nT1VEKrKewH67rfx6MfcM6yKojyyPz7qaqp:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:64",
43+
"yGHXkYLYqxijLKKfd9Q2CB9shRVu8rPNBS53wvwGTutYg4zTg:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:255",
44+
"VByeGLMtP8r8BYQpNX1sb2VtAW8GYCbtFAeXJwsA2ur3MNRdq:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:256",
45+
"yNa8JpqfFB3q8A29rCwSgxvdU94ufJw2yKKxDgznS5m1PoFvn:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d:16383",
46+
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48:42"
47+
},
48+
delimiterString = ":")
49+
void decode(String source, String hex, short prefix) {
50+
val actual = SS58Codec.decode(source);
51+
52+
val expected = AddressWithPrefix.from(HexConverter.toBytes(hex), prefix);
53+
assertEquals(expected, actual);
54+
}
55+
56+
@ParameterizedTest
57+
@CsvSource(value = {
58+
"x", // incorrect length (too short)
59+
"yA3vprfzKUKan9P1eXE6iMGCMSMDZEnAtb6wEjTEf86eMi", // incorrect length (last byte is lost)
60+
"SXYSytZ7wxpQHbRo5FzUFA9wjTfWvTQgYzhVEybWRQvBrhwW", // unknown version (first byte is out of range)
61+
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutL8" // incorrect checksum (second to last byte is different)
62+
})
63+
void decodeThrows(String encoded) {
64+
assertThrows(IllegalArgumentException.class, () -> SS58Codec.decode(encoded));
65+
}
66+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.strategyobject.substrateclient.rpc.core;
2+
3+
import java.util.concurrent.atomic.AtomicReference;
4+
5+
public final class MetadataRegistry {
6+
private static final short DEFAULT_SS58_VERSION = 42;
7+
private static volatile MetadataRegistry instance;
8+
private final AtomicReference<Short> ss58AddressFormat = new AtomicReference<>(DEFAULT_SS58_VERSION); // TODO it should be read from Metadata
9+
10+
private MetadataRegistry() {
11+
}
12+
13+
public static MetadataRegistry getInstance() {
14+
if (instance == null) {
15+
synchronized (MetadataRegistry.class) {
16+
if (instance == null) {
17+
instance = new MetadataRegistry();
18+
}
19+
}
20+
}
21+
22+
return instance;
23+
}
24+
25+
public short getSS58AddressFormat() {
26+
return ss58AddressFormat.get();
27+
}
28+
29+
// TODO it's a workaround to make possible changing network format until metadata is processed
30+
public void setSS58AddressFormat(short value) {
31+
ss58AddressFormat.getAndSet(value);
32+
}
33+
}

rpc/rpc-sections/src/test/java/com/strategyobject/substrateclient/rpc/sections/StateTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
@Testcontainers
2525
public class StateTests {
26-
private static final int WAIT_TIMEOUT = 10;
26+
private static final int WAIT_TIMEOUT = 20;
2727
private static final Network network = Network.newNetwork();
2828

2929
@Container
Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.strategyobject.substrateclient.rpc.sections;
22

3+
import com.strategyobject.substrateclient.common.utils.HexConverter;
34
import com.strategyobject.substrateclient.rpc.codegen.sections.RpcGeneratedSectionFactory;
45
import com.strategyobject.substrateclient.rpc.codegen.sections.RpcInterfaceInitializationException;
5-
import com.strategyobject.substrateclient.rpc.core.registries.RpcEncoderRegistry;
66
import com.strategyobject.substrateclient.rpc.types.AccountId;
77
import com.strategyobject.substrateclient.tests.containers.SubstrateVersion;
88
import com.strategyobject.substrateclient.tests.containers.TestSubstrateContainer;
@@ -18,42 +18,28 @@
1818
import java.util.concurrent.TimeoutException;
1919

2020
import static org.junit.jupiter.api.Assertions.assertEquals;
21-
import static org.mockito.Mockito.*;
2221

2322
@Testcontainers
2423
public class SystemTests {
2524
private static final int WAIT_TIMEOUT = 10;
2625
private static final Network network = Network.newNetwork();
2726

2827
@Container
29-
static final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0)
30-
.withNetwork(network);
28+
static final TestSubstrateContainer substrate = new TestSubstrateContainer(SubstrateVersion.V3_0_0).withNetwork(network);
3129

3230
@Test
3331
void accountNextIndex() throws ExecutionException, InterruptedException, TimeoutException, RpcInterfaceInitializationException {
34-
try (WsProvider wsProvider = WsProvider.builder()
35-
.setEndpoint(substrate.getWsAddress())
36-
.disableAutoConnect()
37-
.build()) {
32+
try (WsProvider wsProvider = WsProvider.builder().setEndpoint(substrate.getWsAddress()).disableAutoConnect().build()) {
3833
wsProvider.connect().get(WAIT_TIMEOUT, TimeUnit.SECONDS);
3934

4035
val sectionFactory = new RpcGeneratedSectionFactory();
4136
System rpcSection = sectionFactory.create(System.class, wsProvider);
4237

43-
// TO DO use registered converter
44-
RpcEncoderRegistry encoderRegistry = mock(RpcEncoderRegistry.class);
45-
when(encoderRegistry.resolve(AccountId.class))
46-
.thenReturn((source, encoders) -> "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY");
47-
try (val utils = mockStatic(RpcEncoderRegistry.class)) {
48-
utils.when(RpcEncoderRegistry::getInstance)
49-
.thenReturn(encoderRegistry);
50-
val result = rpcSection.accountNextIndex(AccountId.fromBytes(
51-
new byte[]{
52-
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
53-
})).get(WAIT_TIMEOUT, TimeUnit.SECONDS);
54-
55-
assertEquals(0, result);
56-
}
38+
val alicePublicKey = HexConverter.toBytes("0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d");
39+
val result = rpcSection.accountNextIndex(AccountId.fromBytes(alicePublicKey))
40+
.get(WAIT_TIMEOUT, TimeUnit.SECONDS);
41+
42+
assertEquals(0, result);
5743
}
5844
}
5945
}

rpc/rpc-types/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ dependencies {
33
implementation project(':scale')
44
implementation project(':rpc:rpc-core')
55
implementation project(':common')
6+
implementation project(':crypto')
67
annotationProcessor project(':rpc:rpc-codegen')
78
annotationProcessor project(':scale:scale-codegen')
89
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.strategyobject.substrateclient.rpc.types;
2+
3+
import com.google.common.base.Preconditions;
4+
import com.strategyobject.substrateclient.crypto.ss58.SS58Codec;
5+
import com.strategyobject.substrateclient.rpc.core.EncoderPair;
6+
import com.strategyobject.substrateclient.rpc.core.MetadataRegistry;
7+
import com.strategyobject.substrateclient.rpc.core.RpcEncoder;
8+
import com.strategyobject.substrateclient.rpc.core.annotations.AutoRegister;
9+
10+
@AutoRegister(types = AccountId.class)
11+
public class AccountIdEncoder implements RpcEncoder<AccountId> {
12+
@Override
13+
public Object encode(AccountId source, EncoderPair<?>... encoders) {
14+
Preconditions.checkArgument(encoders == null || encoders.length == 0);
15+
16+
return SS58Codec.encode(source.getData(), MetadataRegistry.getInstance().getSS58AddressFormat());
17+
}
18+
}

0 commit comments

Comments
 (0)