io.everitoken.sdk.java.EvtLink Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of chain-sdk Show documentation
Show all versions of chain-sdk Show documentation
Official Java SDK for everiToken public chain. https://www.everitoken.io
package io.everitoken.sdk.java;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.*;
import org.apache.commons.lang3.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import io.everitoken.sdk.java.apiResource.EvtLinkStatus;
import io.everitoken.sdk.java.dto.TokenDomain;
import io.everitoken.sdk.java.exceptions.ApiResponseException;
import io.everitoken.sdk.java.exceptions.EvtLinkException;
import io.everitoken.sdk.java.exceptions.EvtLinkSyncTimeException;
import io.everitoken.sdk.java.param.EvtLinkStatusParam;
import io.everitoken.sdk.java.param.NetParams;
import io.everitoken.sdk.java.param.PublicKeysParams;
import io.everitoken.sdk.java.param.RequestParams;
import io.everitoken.sdk.java.provider.SignProviderInterface;
public class EvtLink {
private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$+-/:*";
private static final int BASE = ALPHABET.length();
private static final String QR_PREFIX = "https://evt.li/";
private final NetParams netParams;
private DateTime correctedTime;
public EvtLink(final NetParams netParams) {
this.netParams = netParams;
}
public static EveriPassVerificationResult validateEveriPassUnsafe(NetParams netParams, String link) {
return validateEveriPassUnsafe(netParams, EvtLink.parseLink(link, true));
}
public static EvtLink.Segment findSegmentByType(List segments, int type) {
EvtLink.Segment segment = null;
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getTypeKey() == type) {
segment = segments.get(i);
break;
}
}
return segment;
}
public static EveriPassVerificationResult validateEveriPassUnsafe(NetParams netParams, ParsedLink parsedLink) {
if (!ParsedLink.isEveriPass(parsedLink)) {
throw new EvtLinkException("Flag is not correct for everiPass");
}
// get timestamp
EvtLink.Segment timestampSegment = findSegmentByType(parsedLink.getSegments(), 42);
if (timestampSegment == null) {
throw new EvtLinkException("Failed to parse EveriPass link to extract \"timestamp\"");
}
long timestampInMilli = getUnsignedInt(timestampSegment.getContent()) * 1000;
if (Math.abs(DateTime.now().getMillis() - timestampInMilli) > 6 * 10000) {
throw new EvtLinkException("EveriPass is already expired");
}
// get domain
EvtLink.Segment domainSegment = findSegmentByType(parsedLink.getSegments(), 91);
if (domainSegment == null) {
throw new EvtLinkException("Failed to parse EveriPass link to extract \"domain\"");
}
// get token name
EvtLink.Segment tokenSegment = findSegmentByType(parsedLink.getSegments(), 92);
if (tokenSegment == null) {
throw new EvtLinkException("Failed to parse EveriPass link to extract \"token name\"");
}
String domain = new String(domainSegment.getContent(), StandardCharsets.UTF_8);
String tokenName = new String(tokenSegment.getContent(), StandardCharsets.UTF_8);
if (parsedLink.getPublicKeys().size() != 1) {
throw new EvtLinkException(
"For unsafe validation of everiPass, evtLink must have one and only one " + "signature.");
}
try {
List ownedTokens = new Api(netParams)
.getOwnedTokens(PublicKeysParams.of(parsedLink.getPublicKeys()));
boolean isOwnToken = false;
for (int i = 0; i < ownedTokens.size(); i++) {
TokenDomain token = ownedTokens.get(i);
if (token.getDomain().equals(domain) && token.getName().equals(tokenName)) {
isOwnToken = true;
break;
}
}
return new EveriPassVerificationResult(isOwnToken, domain, tokenName);
} catch (ApiResponseException ex) {
throw new EvtLinkException(
String.format("Can't get owned tokens from the public keys, detailed error: " + "%s", ex.getRaw()),
ex);
}
}
private static List parseSegments(final byte[] segmentBytes) {
final List segments = new ArrayList<>();
int offset = 0;
while (offset < segmentBytes.length) {
final Segment segment = parseSegment(segmentBytes, offset);
offset += segment.getLength();
segments.add(segment);
}
return segments;
}
public static String appendSignautresToEvtLink(@NotNull String evtLink, List signatures) {
if (evtLink.length() == 0) {
throw new IllegalArgumentException("EvtLink can not be empty.");
}
if (evtLink.contains("_")) {
throw new IllegalArgumentException("This EvtLink has already signature segment.");
}
if (signatures.isEmpty()) {
throw new IllegalArgumentException("Signature array is empty.");
}
byte[] signaturesBytes = new byte[] {};
for (final Signature signature : signatures) {
signaturesBytes = ArrayUtils.addAll(signaturesBytes, signature.getBytes());
}
String content = String.format("%s_%s", evtLink, encode(signaturesBytes));
return content;
}
private static String generateQRCode(final int flag, final List segments,
@Nullable final SignProviderInterface signProvider) {
// sort segments based on type key
Collections.sort(segments, new Comparator() {
public int compare(byte[] a, byte[] b) {
return (a[0] & 0xff) - (b[0] & 0xff);
}
});
// put flag in first
byte[] contentBytes = ByteBuffer.allocate(2).putShort((short) flag).array();
for (final byte[] segment : segments) {
contentBytes = ArrayUtils.addAll(contentBytes, segment);
}
String content = encode(contentBytes);
if ((flag & 16) == 16) {
content = QR_PREFIX + content;
}
if (signProvider != null) {
final List signatures = signProvider.sign(Utils.hash(contentBytes));
if (signatures.size() > 3) {
throw new EvtLinkException(
String.format("Only 3 signatures are allowed, \"%d\" passed", signatures.size()));
}
content = appendSignautresToEvtLink(content, signatures);
}
return content;
}
public static ParsedLink parseLink(final String rawContent, final boolean recoverPublicKey) {
int rawContentLength = rawContent.length();
if (rawContentLength < 3 || rawContentLength > 2000) {
throw new EvtLinkException(String.format("Invalid EvtLink length of \"%d\"", rawContentLength));
}
final String[] parts = rawContent.split("_");
// check prefix
if (parts[0].startsWith(QR_PREFIX)) {
parts[0] = parts[0].substring(QR_PREFIX.length());
}
final byte[] contentBytes = EvtLink.decode(parts[0]);
final int flag = ByteBuffer.allocate(2).put(contentBytes, 0, 2).getShort(0) & 0xffff;
final List signatures = new ArrayList<>();
final List publicKeys = new ArrayList<>();
if (parts.length > 1) {
final byte[] sigBytes = EvtLink.decode(parts[1]);
for (int i = 0; i < sigBytes.length; i = i + Signature.BUFFER_LENGTH) {
final byte[] singleSigBytes = ByteBuffer.allocate(Signature.BUFFER_LENGTH)
.put(sigBytes, i, Signature.BUFFER_LENGTH).array();
final Signature sig = Signature.of(singleSigBytes);
signatures.add(sig);
if (recoverPublicKey) {
final PublicKey publicKey = Signature.recoverPublicKey(Utils.hash(contentBytes), sig);
publicKeys.add(publicKey);
}
}
}
return new ParsedLink(flag, parseSegments(ArrayUtils.subarray(contentBytes, 2, contentBytes.length)),
signatures, publicKeys);
}
public static String getUniqueLinkId() {
final SecureRandom random = new SecureRandom();
final byte[] values = new byte[16];
random.nextBytes(values);
return Utils.HEX.encode(values);
}
public static Segment parseSegment(final byte[] content, final int offset) {
final int type = content[offset] & 0xff;
if (type <= 20) {
return new Segment(type, ByteBuffer.allocate(1).put(content[offset + 1]).array(), 2);
} else if (type <= 40) {
return new Segment(type, ByteBuffer.allocate(2).put(content, 1 + offset, 2).array(), 3);
} else if (type <= 90) {
return new Segment(type, ByteBuffer.allocate(4).put(content, 1 + offset, 4).array(), 5);
} else if (type <= 155) {
final int contentLength = content[offset + 1] & 0xff;
return new Segment(type, ByteBuffer.allocate(contentLength).put(content, 2 + offset, contentLength).array(),
contentLength + 2);
} else if (type <= 165) {
final int contentLength = 16;
return new Segment(type, ByteBuffer.allocate(contentLength).put(content, 1 + offset, contentLength).array(),
contentLength + 1);
} else if (type <= 180) {
final int contentLength = content[offset + 1] & 0xff;
return new Segment(type, ByteBuffer.allocate(contentLength).put(content, 2 + offset, contentLength).array(),
contentLength + 2);
} else {
throw new EvtLinkException(String.format("Segment type %d is not supported", type));
}
}
public static byte[] createSegment(final int type, final byte[] content) {
if (type < 0 || type > 255) {
throw new EvtLinkException("Invalid type value, it must be within 0 to 255");
}
if (type <= 20) {
final ByteBuffer b = ByteBuffer.wrap(new byte[2]);
b.put((byte) type);
b.put(content, 0, 1);
return b.array();
} else if (type <= 40) {
final ByteBuffer b = ByteBuffer.wrap(new byte[3]);
b.put((byte) type);
b.put(content, 0, 2);
return b.array();
} else if (type <= 90) {
final ByteBuffer b = ByteBuffer.wrap(new byte[5]);
b.put((byte) type);
b.put(content, 0, 4);
return b.array();
} else if (type <= 155) {
if (content.length > 255) {
throw new IllegalArgumentException("value exceeds limit (255)");
}
final ByteBuffer b = ByteBuffer.wrap(new byte[2]);
b.put((byte) type);
b.put((byte) content.length);
return ArrayUtils.addAll(b.array(), content);
} else if (type <= 165) {
final ByteBuffer b = ByteBuffer.wrap(new byte[1]);
b.put((byte) type);
return ArrayUtils.addAll(b.array(), content);
} else if (type <= 180) {
final ByteBuffer b = ByteBuffer.wrap(new byte[2]);
b.put((byte) type);
b.put((byte) content.length);
return ArrayUtils.addAll(b.array(), content);
} else {
throw new EvtLinkException(String.format("Segment type %d is not supported", type));
}
}
public static String encode(final byte[] input) {
if (input.length == 0) {
return "";
}
// build prefix string buffer
final StringBuilder prefix = new StringBuilder();
for (final byte i : input) {
if ((int) i != 0) {
break;
}
prefix.append("0");
}
BigInteger bigInteger = new BigInteger(1, input);
final BigInteger baseBn = BigInteger.valueOf(BASE);
final StringBuilder sb = new StringBuilder();
while (bigInteger.compareTo(BigInteger.ZERO) > 0) {
final BigInteger mod = bigInteger.mod(baseBn);
bigInteger = bigInteger.subtract(mod).divide(baseBn);
sb.append(ALPHABET.charAt(mod.intValue()));
}
return prefix.append(sb.reverse()).toString();
}
public static byte[] decode(final String base42EncodedString) {
if (base42EncodedString.length() == 0) {
return new byte[] {};
}
BigInteger resultBn = BigInteger.ZERO;
final BigInteger baseBn = BigInteger.valueOf(BASE);
int leadingZerosCount = 0;
for (int i = 0; i < base42EncodedString.length(); i++) {
if (base42EncodedString.charAt(i) != '0') {
break;
}
leadingZerosCount++;
}
for (int i = 0; i < base42EncodedString.length(); i++) {
final char c = base42EncodedString.charAt(i);
final int index = ALPHABET.indexOf(c);
if (ALPHABET.indexOf(c) == -1) {
throw new EvtLinkException(String.format("Illegal character found \"%s\" at index %d", c, i));
}
resultBn = resultBn.multiply(baseBn).add(BigInteger.valueOf(index));
}
return ArrayUtils.addAll(new byte[leadingZerosCount], resultBn.toByteArray());
}
public static long getUnsignedInt(final byte[] bytes) {
return Long.parseUnsignedLong(Utils.HEX.encode(bytes), 16);
}
public String getEvtLinkForPayeeCode(@NotNull final EveriLinkPayeeCodeParam param) {
int flag = 1 + 16;
byte[] addressBytes = createSegment(95, param.getAddress().getBytes());
byte[] fungibleIdBytes = {};
byte[] amountBytes = {};
if (param.getFungibleId() != null) {
fungibleIdBytes = createSegment(45, ByteBuffer.allocate(4).putInt(param.getFungibleId()).array());
}
if (param.getAmount() != null) {
amountBytes = createSegment(96, param.getAmount().getBytes());
}
if (param.getFungibleId() != null && param.getAmount() != null) {
return generateQRCode(flag, Arrays.asList(addressBytes, fungibleIdBytes, amountBytes), null);
}
return generateQRCode(flag, Arrays.asList(addressBytes), null);
}
public String getEvtLinkForEveriPay(@NotNull final EveriPayParam param,
@Nullable final SignProviderInterface signProvider) {
final int flag = 1 + 4;
DateTime localTime = new DateTime();
final byte[] timestampBytes = createSegment(42,
ArrayUtils.subarray(ByteBuffer.allocate(8).putLong(localTime.getMillis() / 1000).array(), 4, 8));
final byte[] symbolBytes = createSegment(44, ByteBuffer.allocate(4).putInt(param.getSymbol()).array());
final byte[] maxAmountBytes;
final long MAX_32_BITS_UNSIGNED_VALUE = 4_294_967_295L;
if (param.getMaxAmount() >= MAX_32_BITS_UNSIGNED_VALUE) {
maxAmountBytes = createSegment(94, Long.toString(param.getMaxAmount()).getBytes());
} else {
// FIXME: Here comes the fucking hack, trying to pack long type in to 4 bytes
final byte[] lBytes = ArrayUtils.subarray(ByteBuffer.allocate(8).putLong(param.getMaxAmount()).array(), 4,
8);
maxAmountBytes = createSegment(43, lBytes);
}
final byte[] linkIdBytes = createSegment(156, Utils.HEX.decode(param.getLinkId()));
ArrayList segmentList = new ArrayList<>();
segmentList.add(timestampBytes);
segmentList.add(symbolBytes);
segmentList.add(maxAmountBytes);
segmentList.add(linkIdBytes);
if (param.hasMemo()) {
segmentList.add(createSegment(98, param.getMemo().getBytes()));
}
if (param.hasRedirect()) {
segmentList.add(createSegment(99, param.getRedirect().getBytes()));
}
return generateQRCode(flag, segmentList, signProvider);
}
public String getEvtLinkForEveriPass(@NotNull final EveriPassParam param,
@Nullable final SignProviderInterface signProvider) {
final int flag = 1 + 2 + (param.isAutoDestroy() ? 8 : 0);
DateTime localTime = new DateTime();
final byte[] timestampBytes = createSegment(42,
ArrayUtils.subarray(ByteBuffer.allocate(8).putLong(localTime.getMillis() / 1000).array(), 4, 8));
final byte[] domainBytes = createSegment(91, param.getDomain().getBytes());
final byte[] tokenBytes = createSegment(92, param.getToken().getBytes());
if (param.hasMemo()) {
byte[] memoBytes = createSegment(98, param.getMemo().getBytes());
return generateQRCode(flag, Arrays.asList(timestampBytes, domainBytes, tokenBytes, memoBytes),
signProvider);
}
return generateQRCode(flag, Arrays.asList(timestampBytes, domainBytes, tokenBytes), signProvider);
}
private DateTime getCorrectedTime() {
if (correctedTime == null) {
try {
final Api api = new Api(netParams);
correctedTime = Utils.getCorrectedTime(api.getInfo().getHeadBlockTime());
} catch (final ApiResponseException ex) {
throw new EvtLinkSyncTimeException("Unable sync time with node", ex);
}
}
return correctedTime;
}
public Map getStatusOfEvtLink(EvtLinkStatusParam params) throws ApiResponseException {
Map rst = new HashMap<>();
boolean isOnline = false;
DateTime startTime = DateTime.now();
rst.put("success", "false");
do {
try {
JSONObject json = new EvtLinkStatus(params.isBlock()).request(RequestParams.of(netParams, () -> {
JSONObject payload = new JSONObject();
payload.put("link_id", params.getLinkId());
return payload.toString();
}));
isOnline = true;
if (json.getString("trx_id") != null && json.getInteger("block_num") > 0) {
rst.put("pending", "false");
rst.put("success", "true");
rst.put("trx_id", json.getString("trx_id"));
rst.put("block_num", Integer.toString(json.getInteger("block_num")));
return rst;
}
} catch (Exception ex) {
if (params.isThrowException()) {
throw new EvtLinkException("EveriPay can not be confirmed", ex);
}
rst.put("pending", "true");
if (ex instanceof ApiResponseException) {
isOnline = true;
rst.put("error", ((ApiResponseException) ex).getRaw().toString());
} else if (ex instanceof JSONException) {
isOnline = true;
rst.put("error", ex.getMessage());
} else {
rst.put("error", ex.getMessage());
}
}
} while (params.isBlock() && (DateTime.now().getMillis() - startTime.getMillis()) < 15000);
if (!isOnline) {
rst.put("pending", "true");
rst.put("error", "Network is not available");
}
return rst;
}
public static class Segment {
private final int typeKey;
private final byte[] content;
private final int length;
private Segment(final int typeKey, final byte[] content, final int length) {
this.typeKey = typeKey;
this.content = content;
this.length = length;
}
public int getTypeKey() {
return typeKey;
}
public byte[] getContent() {
return content;
}
public int getLength() {
return length;
}
}
public static class ParsedLink {
private final List signatures;
private final List publicKeys;
private final List segments;
private final int flag;
private ParsedLink(final int flag, final List segments, final List signatures,
final List publicKeys) {
this.flag = flag;
this.segments = segments;
this.signatures = signatures;
this.publicKeys = publicKeys;
}
public static boolean isEveriPass(ParsedLink link) {
return (link.getFlag() & 2) == 2;
}
public static boolean isEveriPay(ParsedLink link) {
return (link.getFlag() & 4) == 4;
}
public List getSignatures() {
return signatures;
}
public List getPublicKeys() {
return publicKeys;
}
public List getSegments() {
return segments;
}
public int getFlag() {
return flag;
}
}
public static class EveriPayParam {
private final int symbol;
private final String linkId;
private final long maxAmount;
private final String memo;
private final String redirect;
public EveriPayParam(final int symbol, @NotNull final String linkId, final long maxAmount,
@Nullable String memo, @Nullable String redirect) {
if (linkId.length() != 32) {
throw new EvtLinkException(String.format("LinkId must be with length 32, \"%s\" passed", linkId));
}
this.symbol = symbol;
this.linkId = linkId;
this.maxAmount = maxAmount;
this.memo = memo;
this.redirect = redirect;
}
public EveriPayParam(final int symbol, @NotNull final String linkId, final long maxAmount) {
this(symbol, linkId, maxAmount, null, null);
}
public int getSymbol() {
return symbol;
}
public String getLinkId() {
return linkId;
}
public long getMaxAmount() {
return maxAmount;
}
public boolean hasRedirect() {
return redirect != null;
}
public String getRedirect() {
return redirect;
}
public boolean hasMemo() {
return memo != null;
}
public String getMemo() {
return memo;
}
}
public static class EveriPassParam {
private final boolean autoDestroy;
private final String domain;
private final String token;
private final String memo;
public EveriPassParam(final boolean autoDestroy, @NotNull final String domain, @NotNull final String token,
@Nullable String memo) {
this.autoDestroy = autoDestroy;
this.domain = domain;
this.token = token;
this.memo = memo;
}
public EveriPassParam(final boolean autoDestroy, @NotNull final String domain, @NotNull final String token) {
this.autoDestroy = autoDestroy;
this.domain = domain;
this.token = token;
this.memo = null;
}
public boolean isAutoDestroy() {
return autoDestroy;
}
public boolean hasMemo() {
return memo != null;
}
public String getMemo() {
return memo;
}
public String getDomain() {
return domain;
}
public String getToken() {
return token;
}
}
public static class EveriLinkPayeeCodeParam {
private Integer fungibleId;
private Address address;
private String amount;
public EveriLinkPayeeCodeParam(@NotNull Address address, @Nullable Integer fungibleId,
@Nullable String amount) {
if (amount != null) {
Objects.requireNonNull(fungibleId);
}
this.fungibleId = fungibleId;
this.address = address;
this.amount = amount;
}
public EveriLinkPayeeCodeParam(Address address) {
this(address, null, null);
}
public Integer getFungibleId() {
return fungibleId;
}
public String getAddress() {
return address.toString();
}
public String getAmount() {
return amount;
}
}
public static class EveriPassVerificationResult {
private boolean valid;
private String domain;
private String tokenName;
public EveriPassVerificationResult(boolean valid, String domain, String tokenName) {
this.valid = valid;
this.domain = domain;
this.tokenName = tokenName;
}
public boolean isValid() {
return valid;
}
public String getDomain() {
return domain;
}
public String getTokenName() {
return tokenName;
}
}
}