Skip to content

Commit

Permalink
use first 4 bits as type for address
Browse files Browse the repository at this point in the history
  • Loading branch information
boqiu committed Apr 2, 2020
1 parent 796dbbc commit ded53bb
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 48 deletions.
13 changes: 8 additions & 5 deletions src/main/java/conflux/web3j/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.ECKeyPair;

import conflux.web3j.types.AddressType;

public class Account {

Expand All @@ -19,7 +22,7 @@ public class Account {
private BigInteger nonce;

private AccountManager am;
private Credentials credentials;
private ECKeyPair ecKeyPair;

private Account(Cfx cfx, String address) {
this.cfx = cfx;
Expand All @@ -45,8 +48,8 @@ public static Account unlock(Cfx cfx, AccountManager am, String address, String
public static Account create(Cfx cfx, String privateKey) {
Credentials credentials = Credentials.create(privateKey);

Account account = new Account(cfx, credentials.getAddress());
account.credentials = credentials;
Account account = new Account(cfx, AddressType.User.normalize(credentials.getAddress()));
account.ecKeyPair = credentials.getEcKeyPair();

return account;
}
Expand All @@ -56,9 +59,9 @@ public BigInteger getNonce() {
}

public String send(RawTransaction tx) throws Exception {
String signedTx = this.credentials == null
String signedTx = this.ecKeyPair == null
? this.am.signTransaction(tx, this.address)
: tx.sign(this.credentials.getEcKeyPair());
: tx.sign(this.ecKeyPair);
String txHash = this.cfx.sendRawTransaction(signedTx).sendAndGet();

if (txHash == null || txHash.isEmpty()) {
Expand Down
98 changes: 55 additions & 43 deletions src/main/java/conflux/web3j/AccountManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,28 @@
import org.web3j.crypto.CipherException;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.crypto.Wallet;
import org.web3j.crypto.WalletFile;
import org.web3j.crypto.WalletUtils;
import org.web3j.utils.Numeric;

import com.fasterxml.jackson.databind.ObjectMapper;

import conflux.web3j.types.Address;
import conflux.web3j.types.AddressType;

/**
* AccountManager manages Conflux accounts at local file system.
*
*/
public class AccountManager {

private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String keyfilePrefix = "conflux-keyfile-";
private static final String keyfileExt = ".json";

// directory to store the key files.
private String dir;
// unlocked accounts: map<address, item>
Expand Down Expand Up @@ -82,8 +95,18 @@ public static String getDefaultDirectory() {
* @return address of new created account.
*/
public String create(String password) throws Exception {
String filename = WalletUtils.generateNewWalletFile(password, new File(this.dir));
return WalletUtils.loadCredentials(password, new File(this.dir, filename)).getAddress();
return this.createKeyFile(password, Keys.createEcKeyPair());
}

private String createKeyFile(String password, ECKeyPair ecKeyPair) throws Exception {
WalletFile walletFile = Wallet.createStandard(password, ecKeyPair);
walletFile.setAddress(AddressType.User.normalize(walletFile.getAddress()));

String filename = String.format("%s%s%s", keyfilePrefix, walletFile.getAddress(), keyfileExt);
File keyfile = new File(this.dir, filename);
objectMapper.writeValue(keyfile, walletFile);

return walletFile.getAddress();
}

/**
Expand All @@ -104,40 +127,30 @@ public List<String> list() throws IOException {
* @return account address of the key file.
*/
private String parseAddressFromFilename(String filename) {
String prefix = "UTC--";
String ext = ".json";
if (!filename.startsWith(prefix) || !filename.endsWith(ext)) {
if (!filename.startsWith(keyfilePrefix) || !filename.endsWith(keyfileExt)) {
return "";
}

filename = filename.substring(prefix.length(), filename.length() - ext.length());

int index = filename.indexOf("--");
if (index == -1) {
return "";
}
String address = filename.substring(keyfilePrefix.length(), filename.length() - keyfileExt.length());

String address = filename.substring(index + "--".length());
if (address.length() != 40) {
try {
Address.validate(address, AddressType.User);
} catch (Exception e) {
return "";
}

return "0x" + address;
return address;
}

/**
* Import unmanaged account from specified credentials.
* @param credentials credentials to import.
* @param password encrypt the new created/managed key file.
* @return imported account address if not exists. Otherwise, return <code>Optional.empty()</code>.
*/
public Optional<String> imports(Credentials credentials, String password) throws Exception {
if (this.exists(credentials.getAddress())) {
private Optional<String> imports(Credentials credentials, String password) throws Exception {
String address = AddressType.User.normalize(credentials.getAddress());
if (this.exists(address)) {
return Optional.empty();
}

WalletUtils.generateWalletFile(password, credentials.getEcKeyPair(), new File(this.dir), true);
return Optional.of(credentials.getAddress());
this.createKeyFile(password, credentials.getEcKeyPair());

return Optional.of(address);
}

/**
Expand Down Expand Up @@ -214,9 +227,8 @@ public boolean update(String address, String password, String newPassword) throw
}

ECKeyPair ecKeyPair = WalletUtils.loadCredentials(password, files.get(0).toString()).getEcKeyPair();

Files.delete(files.get(0));
WalletUtils.generateWalletFile(newPassword, ecKeyPair, new File(this.dir), true);
this.createKeyFile(newPassword, ecKeyPair);

return true;
}
Expand All @@ -242,9 +254,9 @@ public boolean unlock(String address, String password, Duration... timeout) thro
UnlockedItem item;

if (timeout != null && timeout.length > 0 && timeout[0] != null && timeout[0].compareTo(Duration.ZERO) > 0) {
item = new UnlockedItem(credentials, Optional.of(timeout[0]));
item = new UnlockedItem(credentials.getEcKeyPair(), Optional.of(timeout[0]));
} else {
item = new UnlockedItem(credentials, Optional.empty());
item = new UnlockedItem(credentials.getEcKeyPair(), Optional.empty());
}

this.unlocked.put(address, item);
Expand All @@ -271,11 +283,11 @@ public boolean lock(String address) {
* @exception IllegalArgumentException if account not found, or password not specified for locked account, or password expired for unlocked account.
*/
public String signTransaction(RawTransaction tx, String address, String... password) throws Exception {
Credentials credentials = this.getCredentials(address, password);
return tx.sign(credentials.getEcKeyPair());
ECKeyPair ecKeyPair = this.getEcKeyPair(address, password);
return tx.sign(ecKeyPair);
}

private Credentials getCredentials(String address, String... password) throws IOException, CipherException {
private ECKeyPair getEcKeyPair(String address, String... password) throws IOException, CipherException {
UnlockedItem item = this.unlocked.get(address);

if (password == null || password.length == 0 || password[0] == null || password[0].isEmpty()) {
Expand All @@ -288,11 +300,11 @@ private Credentials getCredentials(String address, String... password) throws IO
throw new IllegalArgumentException("password expired for unlocked account");
}

return item.getCredentials();
return item.getEcKeyPair();
} else {
if (item != null) {
if (!item.expired()) {
return item.getCredentials();
return item.getEcKeyPair();
}

this.unlocked.remove(address);
Expand All @@ -306,17 +318,17 @@ private Credentials getCredentials(String address, String... password) throws IO
throw new IllegalArgumentException("account not found");
}

return WalletUtils.loadCredentials(password[0], files.get(0).toString());
return WalletUtils.loadCredentials(password[0], files.get(0).toString()).getEcKeyPair();
}
}

public String signMessage(byte[] message, boolean needToHash, String address, String... password) throws Exception {
Credentials credentials = this.getCredentials(address, password);
return signMessage(message, needToHash, credentials);
ECKeyPair ecKeyPair = this.getEcKeyPair(address, password);
return signMessage(message, needToHash, ecKeyPair);
}

public static String signMessage(byte[] message, boolean needToHash, Credentials credentials) {
Sign.SignatureData data = Sign.signMessage(message, credentials.getEcKeyPair(), needToHash);
public static String signMessage(byte[] message, boolean needToHash, ECKeyPair ecKeyPair) {
Sign.SignatureData data = Sign.signMessage(message, ecKeyPair, needToHash);

byte[] rsv = new byte[data.getR().length + data.getS().length + data.getV().length];
System.arraycopy(data.getR(), 0, rsv, 0, data.getR().length);
Expand All @@ -328,11 +340,11 @@ public static String signMessage(byte[] message, boolean needToHash, Credentials
}

class UnlockedItem {
private Credentials credentials;
private ECKeyPair ecKeyPair;
private Optional<Instant> until;

public UnlockedItem(Credentials credentials, Optional<Duration> timeout) {
this.credentials = credentials;
public UnlockedItem(ECKeyPair ecKeyPair, Optional<Duration> timeout) {
this.ecKeyPair = ecKeyPair;

if (!timeout.isPresent()) {
this.until = Optional.empty();
Expand All @@ -341,8 +353,8 @@ public UnlockedItem(Credentials credentials, Optional<Duration> timeout) {
}
}

public Credentials getCredentials() {
return credentials;
public ECKeyPair getEcKeyPair() {
return ecKeyPair;
}

public boolean expired() {
Expand Down
41 changes: 41 additions & 0 deletions src/main/java/conflux/web3j/types/Address.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package conflux.web3j.types;

import java.util.Optional;

import org.web3j.utils.Numeric;

public class Address {

private static final int HEX_LENGTH_WITH_PREFIX = 42;

public static void validate(String hexValue) throws AddressException {
validate(hexValue, null);
}

public static void validate(String hexValue, AddressType expectedType) throws AddressException {
if (!Numeric.containsHexPrefix(hexValue)) {
throw AddressException.INVALID_PREFIX;
}

if (hexValue.length() != HEX_LENGTH_WITH_PREFIX) {
throw AddressException.INVALID_LENGTH;
}

Optional<AddressType> type = AddressType.parse(hexValue.charAt(2));
if (!type.isPresent()) {
throw AddressException.INVALID_TYPE;
}

if (expectedType != null && !type.get().equals(expectedType)) {
throw expectedType.getTypeMismatchException();
}

for (int i = 2; i < HEX_LENGTH_WITH_PREFIX; i++) {
char ch = hexValue.charAt(i);
if (ch < '0' || (ch > '9' && ch < 'A') || (ch > 'Z' && ch < 'a') || ch > 'z') {
throw AddressException.INVALID_HEX;
}
}
}

}
27 changes: 27 additions & 0 deletions src/main/java/conflux/web3j/types/AddressException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package conflux.web3j.types;

public class AddressException extends RuntimeException {

private static final long serialVersionUID = 2338294090416527939L;

public static final AddressException INVALID_PREFIX = new AddressException("HEX prefix 0x missed");
public static final AddressException INVALID_LENGTH = new AddressException("wrong length");
public static final AddressException INVALID_TYPE = new AddressException("wrong type");
public static final AddressException INVALID_HEX = new AddressException("wrong HEX format");

private String reason;

public AddressException() {
}

public AddressException(String reason) {
super(String.format("invalid address (%s)", reason));

this.reason = reason;
}

public String getReason() {
return reason;
}

}
41 changes: 41 additions & 0 deletions src/main/java/conflux/web3j/types/AddressType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package conflux.web3j.types;

import java.util.Optional;

import org.web3j.utils.Numeric;

public enum AddressType {
User('1', new AddressException("user address type required")),
Contract('8', new AddressException("contract address type required"));

private char value;
private AddressException typeMismatchException;

private AddressType(char value, AddressException ae) {
this.value = value;
this.typeMismatchException = ae;
}

public char getValue() {
return this.value;
}

public AddressException getTypeMismatchException() {
return typeMismatchException;
}

public String normalize(String address) {
return String.format("0x%s%s", this.value, Numeric.cleanHexPrefix(address).substring(1));
}

public static Optional<AddressType> parse(char ch) {
for (AddressType type : AddressType.values()) {
if (type.value == ch) {
return Optional.of(type);
}
}

return Optional.empty();
}

}

0 comments on commit ded53bb

Please sign in to comment.