From 5398258bdcb596d83d7f43848cefe28944f50039 Mon Sep 17 00:00:00 2001 From: justindg Date: Tue, 9 May 2023 20:45:31 -0700 Subject: [PATCH 01/11] Decode url --- .../java/com/alphawallet/app/util/Utils.java | 61 ++++++++++++ .../app/viewmodel/HomeViewModel.java | 97 ++++++++++--------- 2 files changed, 114 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index c955a502f3..9aa2834d15 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -19,6 +19,7 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.StyleSpan; +import android.util.Base64; import android.util.TypedValue; import android.webkit.URLUtil; @@ -1096,4 +1097,64 @@ public static boolean isAlphaWallet(Context context) { return context.getPackageName().equals("io.stormbird.wallet"); } + + public static boolean hasAttestation(String url) + { + int hashIndex = url.indexOf("#attestation="); + if (hashIndex >= 0) + { + url = url.substring(hashIndex + 13); + } + + return url.length() > 10; + } + + public static String getAttestationString(String url) + { + int hashIndex = url.indexOf("#attestation="); + if (hashIndex >= 0) + { + return url.substring(hashIndex + 13); + } + return ""; + } + + public static byte[] getAttestationBytes(String url) + { + return Base64.decode(getAttestationString(url), Base64.DEFAULT); + } + + public static String getDecodedAttestation(String url) + { + return Hex.byteArrayToHexString(getAttestationBytes(url)); + } + + public static byte[] hexStringToByteArray(String s) + { + //clean prefix + s = s.substring(2); + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) + { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + public static void unzip(String url) + { + String attestation = getAttestationString(url); + byte[] attestationBytes = getAttestationBytes(url); + String decodedAttestation = getDecodedAttestation(url); + String decodedAttestationNoPrefix = decodedAttestation.substring(2); + + Timber.d("attestation: " + attestation); + Timber.d("attestationBytes: " + new String(attestationBytes)); + Timber.d("decodedAttestation: " + decodedAttestation); + Timber.d("decodedAttestationNoPrefix: " + decodedAttestationNoPrefix); + + // TODO: Unzip here + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index dcd4ab4cee..9dbeef55f4 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -80,6 +80,7 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; + import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; @@ -355,61 +356,69 @@ public void setErrorCallback(FragmentMessenger callback) { assetDefinitionService.setErrorCallback(callback); } - + public void handleQRCode(Activity activity, String qrCode) { try { if (qrCode == null) return; - AnalyticsProperties props = new AnalyticsProperties(); - QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); - QRResult qrResult = parser.parse(qrCode); - switch (qrResult.type) + if (Utils.hasAttestation(qrCode)) + { + Utils.unzip(qrCode); + } + else { - case ADDRESS: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - //showSend(activity, qrResult); //For now, direct an ETH address to send screen - //TODO: Issue #1504: bottom-screen popup to choose between: Add to Address book, Sent to Address, or Watch Wallet - showActionSheet(activity, qrResult); - break; - case PAYMENT: - case TRANSFER: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - showSend(activity, qrResult); - break; - case FUNCTION_CALL: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - //TODO: Handle via ConfirmationActivity, need to generate function signature + data then call ConfirmationActivity - //TODO: Code to generate the function signature will look like the code in generateTransactionFunction - break; - case URL: - props.put(QrScanResultType.KEY, QrScanResultType.URL.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - ((HomeActivity) activity).onBrowserWithURL(qrCode); - break; - case MAGIC_LINK: - showImportLink(activity, qrCode); - break; - case OTHER: - qrCode = null; - break; - case OTHER_PROTOCOL: - break; - case ATTESTATION: - ((HomeActivity)activity).importAttestation(qrResult); - break; + AnalyticsProperties props = new AnalyticsProperties(); + QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); + QRResult qrResult = parser.parse(qrCode); + switch (qrResult.type) + { + case ADDRESS: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + //showSend(activity, qrResult); //For now, direct an ETH address to send screen + //TODO: Issue #1504: bottom-screen popup to choose between: Add to Address book, Sent to Address, or Watch Wallet + showActionSheet(activity, qrResult); + break; + case PAYMENT: + case TRANSFER: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + showSend(activity, qrResult); + break; + case FUNCTION_CALL: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + //TODO: Handle via ConfirmationActivity, need to generate function signature + data then call ConfirmationActivity + //TODO: Code to generate the function signature will look like the code in generateTransactionFunction + break; + case URL: + props.put(QrScanResultType.KEY, QrScanResultType.URL.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + ((HomeActivity) activity).onBrowserWithURL(qrCode); + break; + case MAGIC_LINK: + showImportLink(activity, qrCode); + break; + case OTHER: + qrCode = null; + break; + case OTHER_PROTOCOL: + break; + case ATTESTATION: + ((HomeActivity)activity).importAttestation(qrResult); + break; + } } } catch (Exception e) { + Timber.e(e); qrCode = null; } From d197a5cdfd119a1a546c45ec8174dca9105c5b36 Mon Sep 17 00:00:00 2001 From: justindg Date: Tue, 9 May 2023 22:32:30 -0700 Subject: [PATCH 02/11] Inflate deflated data --- .../java/com/alphawallet/app/util/Utils.java | 93 +++++++++++++------ 1 file changed, 63 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 9aa2834d15..2ec8c1a116 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -45,14 +45,17 @@ import org.web3j.rlp.RlpString; import org.web3j.utils.Numeric; +import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.net.URI; +import java.net.URLDecoder; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.text.DateFormat; @@ -65,6 +68,7 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.zip.Inflater; import timber.log.Timber; @@ -1114,47 +1118,76 @@ public static String getAttestationString(String url) int hashIndex = url.indexOf("#attestation="); if (hashIndex >= 0) { - return url.substring(hashIndex + 13); + url = url.substring(hashIndex + 13); + } + try + { + String decoded = URLDecoder.decode(url, StandardCharsets.UTF_8.name()); + Timber.d("decoded url: " + decoded); + return decoded; + } + catch (UnsupportedEncodingException e) + { + Timber.e(e); + return ""; } - return ""; } - public static byte[] getAttestationBytes(String url) - { - return Base64.decode(getAttestationString(url), Base64.DEFAULT); - } +// public static byte[] getAttestationBytes(String url) +// { +// return Base64.decode(getAttestationString(url), Base64.DEFAULT); +// } +// +// public static String getDecodedAttestation(String url) +// { +// return Hex.byteArrayToHexString(getAttestationBytes(url)); +// } - public static String getDecodedAttestation(String url) + public static void unzip(String url) { - return Hex.byteArrayToHexString(getAttestationBytes(url)); +// byte[] attestationBytes = getAttestationBytes(url); +// String decodedAttestation = getDecodedAttestation(url); +// String decodedAttestationNoPrefix = decodedAttestation.substring(2); + + String attestation = getAttestationString(url); + + Timber.d("decompressed: " + decompress(attestation)); } - public static byte[] hexStringToByteArray(String s) + public static String decompress(String deflatedData) { - //clean prefix - s = s.substring(2); - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) + byte[] deflatedBytes; + + deflatedBytes = Base64.decode(deflatedData, Base64.DEFAULT); + + Inflater inflater = new Inflater(); + inflater.setInput(deflatedBytes); + + byte[] inflatedData; + + try { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } + // Inflate the data + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + while (!inflater.finished()) + { + int inflatedBytes = inflater.inflate(buffer); + outputStream.write(buffer, 0, inflatedBytes); + } + inflater.end(); - public static void unzip(String url) - { - String attestation = getAttestationString(url); - byte[] attestationBytes = getAttestationBytes(url); - String decodedAttestation = getDecodedAttestation(url); - String decodedAttestationNoPrefix = decodedAttestation.substring(2); + inflatedData = outputStream.toByteArray(); - Timber.d("attestation: " + attestation); - Timber.d("attestationBytes: " + new String(attestationBytes)); - Timber.d("decodedAttestation: " + decodedAttestation); - Timber.d("decodedAttestationNoPrefix: " + decodedAttestationNoPrefix); + // Convert the inflated bytes to a string + String inflatedString = new String(inflatedData); - // TODO: Unzip here + return inflatedString; + } + catch (Exception e) + { + Timber.e(e); + return ""; + } } } From 315183f64a1cb239bda5c3b062ca14d0c63ded32 Mon Sep 17 00:00:00 2001 From: justindg Date: Tue, 9 May 2023 23:08:06 -0700 Subject: [PATCH 03/11] Move checking to QRParser --- .../alphawallet/app/entity/EIP681Type.java | 3 +- .../com/alphawallet/app/util/QRParser.java | 14 ++- .../java/com/alphawallet/app/util/Utils.java | 22 +--- .../app/viewmodel/HomeViewModel.java | 100 +++++++++--------- 4 files changed, 64 insertions(+), 75 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/entity/EIP681Type.java b/app/src/main/java/com/alphawallet/app/entity/EIP681Type.java index 398650eae2..aec1f8ee94 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EIP681Type.java +++ b/app/src/main/java/com/alphawallet/app/entity/EIP681Type.java @@ -14,5 +14,6 @@ public enum EIP681Type MAGIC_LINK, OTHER_PROTOCOL, ATTESTATION, - OTHER + OTHER, + EAS_ATTESTATION } diff --git a/app/src/main/java/com/alphawallet/app/util/QRParser.java b/app/src/main/java/com/alphawallet/app/util/QRParser.java index 679035579e..59a0b10835 100644 --- a/app/src/main/java/com/alphawallet/app/util/QRParser.java +++ b/app/src/main/java/com/alphawallet/app/util/QRParser.java @@ -1,6 +1,5 @@ package com.alphawallet.app.util; -import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.EIP681Type; import com.alphawallet.app.entity.EthereumProtocolParser; import com.alphawallet.app.entity.QRResult; @@ -9,7 +8,6 @@ import com.alphawallet.token.entity.MagicLinkInfo; import com.alphawallet.token.tools.Numeric; -import java.math.BigDecimal; import java.math.BigInteger; import java.net.URL; import java.util.ArrayList; @@ -87,10 +85,20 @@ private static String extractAddress(String str) public QRResult parse(String url) { + QRResult result = null; + if (url == null) return null; + + if (Utils.hasAttestation(url)) + { + result = new QRResult(url); + result.type = EIP681Type.EAS_ATTESTATION; + result.functionDetail = Utils.decompress(url); + return result; + } + String[] parts = url.split(":"); - QRResult result = null; //Check for import/magic link if (checkForMagicLink(url)) diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 2ec8c1a116..9a27894036 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -1133,28 +1133,12 @@ public static String getAttestationString(String url) } } -// public static byte[] getAttestationBytes(String url) -// { -// return Base64.decode(getAttestationString(url), Base64.DEFAULT); -// } -// -// public static String getDecodedAttestation(String url) -// { -// return Hex.byteArrayToHexString(getAttestationBytes(url)); -// } - - public static void unzip(String url) + public static String decompress(String url) { -// byte[] attestationBytes = getAttestationBytes(url); -// String decodedAttestation = getDecodedAttestation(url); -// String decodedAttestationNoPrefix = decodedAttestation.substring(2); - - String attestation = getAttestationString(url); - - Timber.d("decompressed: " + decompress(attestation)); + return inflateData(getAttestationString(url)); } - public static String decompress(String deflatedData) + public static String inflateData(String deflatedData) { byte[] deflatedBytes; diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index 9dbeef55f4..0682c4f77c 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -80,7 +80,6 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; - import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; @@ -356,65 +355,62 @@ public void setErrorCallback(FragmentMessenger callback) { assetDefinitionService.setErrorCallback(callback); } - + public void handleQRCode(Activity activity, String qrCode) { try { if (qrCode == null) return; - if (Utils.hasAttestation(qrCode)) - { - Utils.unzip(qrCode); - } - else + AnalyticsProperties props = new AnalyticsProperties(); + QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); + QRResult qrResult = parser.parse(qrCode); + switch (qrResult.type) { - AnalyticsProperties props = new AnalyticsProperties(); - QRParser parser = QRParser.getInstance(EthereumNetworkBase.extraChains()); - QRResult qrResult = parser.parse(qrCode); - switch (qrResult.type) - { - case ADDRESS: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - //showSend(activity, qrResult); //For now, direct an ETH address to send screen - //TODO: Issue #1504: bottom-screen popup to choose between: Add to Address book, Sent to Address, or Watch Wallet - showActionSheet(activity, qrResult); - break; - case PAYMENT: - case TRANSFER: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - showSend(activity, qrResult); - break; - case FUNCTION_CALL: - props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - //TODO: Handle via ConfirmationActivity, need to generate function signature + data then call ConfirmationActivity - //TODO: Code to generate the function signature will look like the code in generateTransactionFunction - break; - case URL: - props.put(QrScanResultType.KEY, QrScanResultType.URL.getValue()); - track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - ((HomeActivity) activity).onBrowserWithURL(qrCode); - break; - case MAGIC_LINK: - showImportLink(activity, qrCode); - break; - case OTHER: - qrCode = null; - break; - case OTHER_PROTOCOL: - break; - case ATTESTATION: - ((HomeActivity)activity).importAttestation(qrResult); - break; - } + case EAS_ATTESTATION: + // TODO: + break; + case ADDRESS: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + //showSend(activity, qrResult); //For now, direct an ETH address to send screen + //TODO: Issue #1504: bottom-screen popup to choose between: Add to Address book, Sent to Address, or Watch Wallet + showActionSheet(activity, qrResult); + break; + case PAYMENT: + case TRANSFER: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + showSend(activity, qrResult); + break; + case FUNCTION_CALL: + props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS_OR_EIP_681.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + //TODO: Handle via ConfirmationActivity, need to generate function signature + data then call ConfirmationActivity + //TODO: Code to generate the function signature will look like the code in generateTransactionFunction + break; + case URL: + props.put(QrScanResultType.KEY, QrScanResultType.URL.getValue()); + track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); + + ((HomeActivity) activity).onBrowserWithURL(qrCode); + break; + case MAGIC_LINK: + showImportLink(activity, qrCode); + break; + case OTHER: + qrCode = null; + break; + case OTHER_PROTOCOL: + break; + case ATTESTATION: + ((HomeActivity) activity).importAttestation(qrResult); + break; } + } catch (Exception e) { From febfd1f30d3c8423c930811af75cbe631a63ed30 Mon Sep 17 00:00:00 2001 From: justindg Date: Tue, 9 May 2023 23:35:19 -0700 Subject: [PATCH 04/11] Convert result into EAS Attestation json --- .../app/entity/EasAttestation.java | 202 ++++++++++++++++++ .../java/com/alphawallet/app/util/Utils.java | 43 +++- 2 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/alphawallet/app/entity/EasAttestation.java diff --git a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java new file mode 100644 index 0000000000..643bcc62f8 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java @@ -0,0 +1,202 @@ +package com.alphawallet.app.entity; + + +public class EasAttestation +{ + public String version; + public long chainId; + public String verifyingContract; + public String r; + public String s; + public long v; + public String recipient; + public String uid; + public String schema; + public String signer; + public long time; + public long expirationTime; + public String refUID; + public boolean revocable; + public String data; + public long nonce; + + public EasAttestation(String version, long chainId, String verifyingContract, String r, String s, long v, String recipient, String uid, String schema, String signer, long time, long expirationTime, String refUID, boolean revocable, String data, long nonce) + { + this.version = version; + this.chainId = chainId; + this.verifyingContract = verifyingContract; + this.r = r; + this.s = s; + this.v = v; + this.recipient = recipient; + this.uid = uid; + this.schema = schema; + this.signer = signer; + this.time = time; + this.expirationTime = expirationTime; + this.refUID = refUID; + this.revocable = revocable; + this.data = data; + this.nonce = nonce; + } + + public String getVersion() + { + return version; + } + + public void setVersion(String version) + { + this.version = version; + } + + public long getChainId() + { + return chainId; + } + + public void setChainId(long chainId) + { + this.chainId = chainId; + } + + public String getVerifyingContract() + { + return verifyingContract; + } + + public void setVerifyingContract(String verifyingContract) + { + this.verifyingContract = verifyingContract; + } + + public String getR() + { + return r; + } + + public void setR(String r) + { + this.r = r; + } + + public String getS() + { + return s; + } + + public void setS(String s) + { + this.s = s; + } + + public long getV() + { + return v; + } + + public void setV(long v) + { + this.v = v; + } + + public String getRecipient() + { + return recipient; + } + + public void setRecipient(String recipient) + { + this.recipient = recipient; + } + + public String getUid() + { + return uid; + } + + public void setUid(String uid) + { + this.uid = uid; + } + + public String getSchema() + { + return schema; + } + + public void setSchema(String schema) + { + this.schema = schema; + } + + public String getSigner() + { + return signer; + } + + public void setSigner(String signer) + { + this.signer = signer; + } + + public long getTime() + { + return time; + } + + public void setTime(long time) + { + this.time = time; + } + + public long getExpirationTime() + { + return expirationTime; + } + + public void setExpirationTime(long expirationTime) + { + this.expirationTime = expirationTime; + } + + public String getRefUID() + { + return refUID; + } + + public void setRefUID(String refUID) + { + this.refUID = refUID; + } + + public boolean isRevocable() + { + return revocable; + } + + public void setRevocable(boolean revocable) + { + this.revocable = revocable; + } + + public String getData() + { + return data; + } + + public void setData(String data) + { + this.data = data; + } + + public long getNonce() + { + return nonce; + } + + public void setNonce(long nonce) + { + this.nonce = nonce; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 9a27894036..659085ea10 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -29,11 +29,13 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.util.pattern.Patterns; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.ProviderTypedData; import com.alphawallet.token.entity.Signable; +import com.google.gson.Gson; import org.jetbrains.annotations.NotNull; import org.json.JSONObject; @@ -1135,7 +1137,46 @@ public static String getAttestationString(String url) public static String decompress(String url) { - return inflateData(getAttestationString(url)); + Timber.d(toAttestationJson(inflateData(getAttestationString(url)))); + return toAttestationJson(inflateData(getAttestationString(url))); +// return inflateData(getAttestationString(url)); + } + + private static String toAttestationJson(String jsonString) + { + // Remove the square brackets + jsonString = jsonString.substring(1, jsonString.length() - 1); + String[] e = jsonString.split(","); + + // Clean the strings + for (int i = 0; i < e.length; i++) { + e[i] = e[i].trim(); + e[i] = e[i].replaceAll("\"", ""); + } + + EasAttestation easAttestation = + new EasAttestation( + e[0], + Long.parseLong(e[1]), + e[2], + e[3], + e[4], + Long.parseLong(e[5]), + e[6], + e[7], + e[8], + e[9], + Long.parseLong(e[10]), + Long.parseLong(e[11]), + e[12], + Boolean.parseBoolean(e[13]), + e[14], + Long.parseLong(e[15]) + ); + + String attestationJson = new Gson().toJson(easAttestation); + + return attestationJson; } public static String inflateData(String deflatedData) From ebdc5ad56c09eeceac6fdd9a6516a13c2ed6947d Mon Sep 17 00:00:00 2001 From: James Brown Date: Fri, 12 May 2023 20:24:50 +1000 Subject: [PATCH 05/11] - Handle EAS Attestations stage 1 --- .../app/entity/EasAttestation.java | 96 ++++++++++++++++++- .../app/service/AssetDefinitionService.java | 77 +++++++++++++++ .../com/alphawallet/app/ui/HomeActivity.java | 10 ++ .../alphawallet/app/ui/WalletFragment.java | 5 + .../app/viewmodel/HomeViewModel.java | 3 +- .../app/viewmodel/WalletViewModel.java | 50 +++++++++- 6 files changed, 232 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java index 643bcc62f8..2c38204d5c 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java @@ -1,6 +1,15 @@ package com.alphawallet.app.entity; +import com.alphawallet.token.tools.Numeric; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.math.BigInteger; + +import timber.log.Timber; + public class EasAttestation { public String version; @@ -20,7 +29,7 @@ public class EasAttestation public String data; public long nonce; - public EasAttestation(String version, long chainId, String verifyingContract, String r, String s, long v, String recipient, String uid, String schema, String signer, long time, long expirationTime, String refUID, boolean revocable, String data, long nonce) + public EasAttestation(String version, long chainId, String verifyingContract, String r, String s, long v, String signer, String uid, String schema, String recipient, long time, long expirationTime, String refUID, boolean revocable, String data, long nonce) { this.version = version; this.chainId = chainId; @@ -122,7 +131,14 @@ public void setUid(String uid) public String getSchema() { - return schema; + if (schema.equals("0")) + { + return Numeric.toHexStringWithPrefixZeroPadded(BigInteger.ZERO, 64); + } + else + { + return schema; + } } public void setSchema(String schema) @@ -162,7 +178,14 @@ public void setExpirationTime(long expirationTime) public String getRefUID() { - return refUID; + if (refUID.equals("0")) + { + return Numeric.toHexStringWithPrefixZeroPadded(BigInteger.ZERO, 64); + } + else + { + return refUID; + } } public void setRefUID(String refUID) @@ -199,4 +222,69 @@ public void setNonce(long nonce) { this.nonce = nonce; } -} \ No newline at end of file + + public String getEIP712Attestation() + { + JSONObject eip712 = new JSONObject(); + + try + { + JSONObject types = new JSONObject(); + JSONArray jsonType = new JSONArray(); + putElement(jsonType, "name", "string"); + putElement(jsonType, "version", "string"); + putElement(jsonType, "chainId", "uint256"); + putElement(jsonType, "verifyingContract", "address"); + types.put("EIP712Domain", jsonType); + + JSONArray attest = new JSONArray(); + putElement(attest, "schema", "bytes32"); + putElement(attest, "recipient", "address"); + putElement(attest, "time", "uint64"); + putElement(attest, "expirationTime", "uint64"); + putElement(attest, "revocable", "bool"); + putElement(attest, "refUID", "bytes32"); + putElement(attest, "data", "bytes"); + + types.put("Attest", attest); + + eip712.put("types", types); + + JSONObject jsonDomain = new JSONObject(); + jsonDomain.put("name", "EAS Attestation"); + jsonDomain.put("version", version); + jsonDomain.put("chainId", chainId); + jsonDomain.put("verifyingContract", verifyingContract); + + //"primaryType": "Attest", + eip712.put("primaryType", "Attest"); + eip712.put("domain", jsonDomain); + + JSONObject jsonMessage = new JSONObject(); + jsonMessage.put("time", time); + jsonMessage.put("data", data); + jsonMessage.put("expirationTime", expirationTime); + jsonMessage.put("recipient", recipient); + jsonMessage.put("refUID", getRefUID()); + jsonMessage.put("revocable", revocable); + jsonMessage.put("schema", getSchema()); + + eip712.put("message", jsonMessage); + } + catch (Exception e) + { + Timber.e(e); + } + + return eip712.toString(); + } + + private void putElement(JSONArray jsonType, String name, String type) throws Exception + { + JSONObject element = new JSONObject(); + element.put("name", name); + element.put("type", type); + + jsonType.put(element); + } +} diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index b02c1f8c65..37989eecf7 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -26,6 +26,7 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.ContractType; +import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.QueryResponse; import com.alphawallet.app.entity.TokenLocator; @@ -44,6 +45,8 @@ import com.alphawallet.app.ui.HomeActivity; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.HomeViewModel; +import com.alphawallet.app.web3j.StructuredData; +import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.ethereum.NetworkInfo; import com.alphawallet.token.entity.ActionModifier; @@ -76,6 +79,7 @@ import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.EthBlock; @@ -2993,4 +2997,77 @@ public Attestation validateAttestation(String attestation, TokenInfo tInfo) return att; } + + public Attestation validateAttestation(EasAttestation attestation) + { + Attestation att = null; + //1. Resolve UID. For now, just use default: This should be on a switch for chains + String defaultUID = "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"; + String recoverAttestationSigner = recoverSigner(attestation); + + //1. Validate signer via key attestation service (using UID). + //2. Decode the ABI encoded payload. + //3. + + // + + + + + + + /*NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(tInfo.chainId); + att = new Attestation(tInfo, networkInfo.name, Numeric.hexStringToByteArray(attestation)); + att.setTokenWallet(tokensService.getCurrentAddress()); + + //call validation function and get details + TokenDefinition.Attestation definitionAtt = td.getAttestation(); + //can we get the details? + + if (definitionAtt != null && definitionAtt.function != null) + { + //pull return type + FunctionDefinition fd = definitionAtt.function; + //add attestation to attr map + //call function + org.web3j.abi.datatypes.Function transaction = tokenscriptUtility.generateTransactionFunction(att, BigInteger.ZERO, td, fd, this); + transaction = new Function(fd.method, transaction.getInputParameters(), td.getAttestationReturnTypes()); //set return types + + //call and handle result + String result = tokenscriptUtility.callSmartContract(tInfo.chainId, tInfo.address, transaction); + + //break down result + List values = FunctionReturnDecoder.decode(result, transaction.getOutputParameters()); + + //interpret these values + att.handleValidation(td.getValidation(values)); + }*/ + + return att; + } + + private String recoverSigner(EasAttestation attestation) + { + String recoveredAddress = ""; + + try + { + StructuredDataEncoder dataEncoder = new StructuredDataEncoder(attestation.getEIP712Attestation()); + byte[] hash = dataEncoder.hashStructuredData(); + byte[] r = Numeric.hexStringToByteArray(attestation.getR()); + byte[] s = Numeric.hexStringToByteArray(attestation.getS()); + byte v = (byte)(attestation.getV() & 0xFF); + + Sign.SignatureData sig = new Sign.SignatureData(v, r, s); + + BigInteger key = Sign.signedMessageHashToKey(hash, sig); + recoveredAddress = "0x" + Keys.getAddress(key); + } + catch (Exception e) + { + e.printStackTrace(); + } + + return recoveredAddress; + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java index 03baf71ebd..d4abd51651 100644 --- a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java @@ -1297,4 +1297,14 @@ public void importAttestation(QRResult attestation) ((WalletFragment)getFragment(WALLET)).importAttestation(attestation); } + + public void importEASAttestation(QRResult attestation) + { + if (attestation.type != EIP681Type.EAS_ATTESTATION) + { + return; + } + + ((WalletFragment)getFragment(WALLET)).importEASAttestation(attestation); + } } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java index a861338e5c..23981ccbbf 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java @@ -789,6 +789,11 @@ public void importAttestation(QRResult attestation) viewModel.importAttestation(attestation); } + public void importEASAttestation(QRResult attestation) + { + viewModel.importEASAttestation(attestation); + } + private void attestationError(String message) { if (dialog == null) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index 0682c4f77c..a8d0861f15 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -368,7 +368,7 @@ public void handleQRCode(Activity activity, String qrCode) switch (qrResult.type) { case EAS_ATTESTATION: - // TODO: + ((HomeActivity) activity).importEASAttestation(qrResult); break; case ADDRESS: props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); @@ -426,7 +426,6 @@ public void handleQRCode(Activity activity, String qrCode) private void showActionSheet(Activity activity, QRResult qrResult) { - View.OnClickListener listener = v -> { if (v.getId() == R.id.send_to_this_address_action) { diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 6d6ecd3b0f..0e9351d1fa 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -2,7 +2,6 @@ import static com.alphawallet.app.C.EXTRA_ADDRESS; import static com.alphawallet.app.repository.TokensRealmSource.ADDRESS_FORMAT; -import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; import static com.alphawallet.app.widget.CopyTextView.KEY_ADDRESS; import android.app.Activity; @@ -23,6 +22,7 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.ContractType; +import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; @@ -44,7 +44,6 @@ import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.repository.WalletItem; import com.alphawallet.app.repository.entity.RealmAttestation; -import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.router.CoinbasePayRouter; import com.alphawallet.app.router.ManageWalletsRouter; @@ -62,6 +61,7 @@ import com.alphawallet.token.entity.AttestationValidationStatus; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.gson.Gson; import org.jetbrains.annotations.NotNull; import org.web3j.crypto.Keys; @@ -79,7 +79,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import io.realm.Case; import io.realm.Realm; import io.realm.RealmResults; @@ -667,4 +666,49 @@ private Attestation setBaseType(Attestation attn, TokenInfo info) return attn; } + + public void importEASAttestation(QRResult qrAttn) + { + //validate attestation + //get chain and address + EasAttestation easAttn = new Gson().fromJson(qrAttn.functionDetail, EasAttestation.class); + + //validation UID: + + storeAttestation(easAttn) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(attn -> completeImport(easAttn, attn), this::onError) + .isDisposed(); + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + private Single storeAttestation(EasAttestation attestation) + { + //Use Default key unless specified + Attestation attn = assetDefinitionService.validateAttestation(attestation); + switch (attn.isValid()) + { + case Pass: + //return storeAttestationInternal(attestation, attn); + case Expired: + case Issuer_Not_Valid: + case Incorrect_Subject: + attestationError.postValue(attn.isValid().getValue()); + break; + } + + return Single.fromCallable(() -> attn); + } + + private void completeImport(EasAttestation attestation, Attestation tokenAttn) + { + /*if (tokenAttn.isValid() == AttestationValidationStatus.Pass) + { + TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), + assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationId()); + tcmAttestation.isEnabled = true; + updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); + }*/ + } } From 52a5b9c2bec63344b8334689951e06113c8863ff Mon Sep 17 00:00:00 2001 From: James Brown Date: Sat, 13 May 2023 10:57:08 +1000 Subject: [PATCH 06/11] Fix a few issues and improve legacy import --- .../app/entity/tokens/Attestation.java | 8 +++++- .../app/service/TokensService.java | 5 ++++ .../app/ui/NFTAssetDetailActivity.java | 18 ++++++------ .../alphawallet/app/ui/WalletFragment.java | 13 +++++++-- .../app/ui/widget/adapter/TokensAdapter.java | 26 ++++++++++++++++++ .../java/com/alphawallet/app/util/Utils.java | 7 +++-- .../app/viewmodel/WalletViewModel.java | 20 +++++++++++++- app/src/main/res/drawable/zero_one_block.png | Bin 0 -> 276998 bytes .../token/entity/AttestationValidation.java | 12 ++++++-- .../token/tools/TokenDefinition.java | 3 ++ 10 files changed, 95 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/zero_one_block.png diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index b97a8e66b6..4d3b950d00 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -27,6 +27,7 @@ public class Attestation extends Token private BigInteger attestationId; private String attestationSubject; private String issuerKey; + private boolean issuerValid; private String issuerAddress; private long validFrom; private long validUntil; @@ -61,6 +62,7 @@ public void handleValidation(AttestationValidation attValidation) isValid = attValidation._isValid; additionalMembers = attValidation.additionalMembers; issuerKey = attValidation._issuerKey; + issuerValid = attValidation._issuerValid || (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))); } public AttestationValidationStatus isValid() @@ -78,10 +80,14 @@ public AttestationValidationStatus isValid() } //Check issuer - if not valid issuer fail. - if (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))) + if (!issuerValid) { return AttestationValidationStatus.Issuer_Not_Valid; } +// if (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))) +// { +// +// } return AttestationValidationStatus.Pass; } diff --git a/app/src/main/java/com/alphawallet/app/service/TokensService.java b/app/src/main/java/com/alphawallet/app/service/TokensService.java index cb3b433914..a3c6d75793 100644 --- a/app/src/main/java/com/alphawallet/app/service/TokensService.java +++ b/app/src/main/java/com/alphawallet/app/service/TokensService.java @@ -564,6 +564,11 @@ public Single storeTokenInfo(Wallet wallet, TokenInfo tInfo, Contract .flatMap(contractType -> tokenRepository.storeTokenInfo(wallet, tInfo, type)); } + public Single storeTokenInfoDirect(Wallet wallet, TokenInfo tInfo, ContractType type) + { + return tokenRepository.storeTokenInfo(wallet, tInfo, type); + } + //Fix undermined contract type private ContractType checkDefaultType(ContractType contractType, ContractType defaultType) { diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index 312747c662..e1ea6b2499 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -256,7 +256,7 @@ private Token resolveAssetToken() { if (asset != null && asset.isAttestation()) { - return viewModel.getTokenService().getAttestation(chainId, token.getAddress(), tokenId); + return viewModel.getTokenService().getAttestation(chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS), tokenId); } else { @@ -305,7 +305,6 @@ private void setup() viewModel.checkTokenScriptValidity(token); setTitle(token.tokenInfo.name); updateDefaultTokenData(); - viewModel.getAsset(token, tokenId); if (asset != null && asset.isAttestation()) { @@ -313,6 +312,7 @@ private void setup() } else { + viewModel.getAsset(token, tokenId); viewModel.updateLocalAttributes(token, tokenId); } } @@ -442,19 +442,19 @@ private void updateDefaultTokenData() } } - private void loadAssetFromMetadata(NFTAsset asset) + private void loadAssetFromMetadata(NFTAsset loadedAsset) { - if (asset != null) + if (loadedAsset != null) { - updateTokenImage(asset); + updateTokenImage(loadedAsset); - addMetaDataInfo(asset); + addMetaDataInfo(loadedAsset); - nftAttributeLayout.bind(token, asset); + nftAttributeLayout.bind(token, loadedAsset); clearRefreshAnimation(); - loadFromOpenSeaData(asset.getOpenSeaAsset()); + loadFromOpenSeaData(loadedAsset.getOpenSeaAsset()); final List attrs = new ArrayList<>(); @@ -591,7 +591,7 @@ private void onOpenSeaAsset(OpenSeaAsset openSeaAsset) private void setupAttestation() { - tokenImage.setImageResource(R.drawable.zero_one); + tokenImage.setImageResource(R.drawable.zero_one_block); progressBar.setVisibility(View.GONE); } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java index 23981ccbbf..fb9f64f95d 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java @@ -850,8 +850,17 @@ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int position) else if (viewHolder instanceof TokenHolder) { Token token = ((TokenHolder) viewHolder).token; - viewModel.setTokenEnabled(token, false); - SortedItem removedToken = adapter.removeToken(token.tokenInfo.chainId, token.getAddress()); + SortedItem removedToken; + if (token.getInterfaceSpec() == ContractType.ATTESTATION) + { + viewModel.removeAttestation(token); + removedToken = adapter.removeAttestation(token); + } + else + { + viewModel.setTokenEnabled(token, false); + removedToken = adapter.removeToken(token.tokenInfo.chainId, token.getAddress()); + } if (getContext() != null) { diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java index 57d7ca3d49..07b1c31be9 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java @@ -13,6 +13,8 @@ import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.TokenFilter; import com.alphawallet.app.entity.tokendata.TokenGroup; +import com.alphawallet.app.entity.tokens.Attestation; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; import com.alphawallet.app.repository.TokensMappingRepository; @@ -390,6 +392,30 @@ public SortedItem removeToken(long chainId, String tokenAddress) return null; } + //TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), + // assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationId()); + // tcmAttestation.isEnabled = true; + public SortedItem removeAttestation(Token token) + { + Attestation attn = (Attestation)token; + for (int i = 0; i < items.size(); i++) + { + Object si = items.get(i); + if (si instanceof TokenSortedItem) + { + TokenSortedItem tsi = (TokenSortedItem) si; + TokenCardMeta thisToken = tsi.value; + //Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getTokenID()); + if (thisToken.getTokenID().compareTo(attn.getAttestationId()) == 0 && thisToken.getAddress().equalsIgnoreCase(token.getAddress()) + && thisToken.getChain() == token.tokenInfo.chainId) + { + return items.removeItemAt(i); + } + } + } + return null; + } + private boolean canDisplayToken(TokenCardMeta token) { if (token == null) return false; diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 659085ea10..90e830a215 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -1110,9 +1110,12 @@ public static boolean hasAttestation(String url) if (hashIndex >= 0) { url = url.substring(hashIndex + 13); + return url.length() > 10; + } + else + { + return false; } - - return url.length() > 10; } public static String getAttestationString(String url) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 0e9351d1fa..a6aef6324b 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -573,6 +573,7 @@ public void importAttestation(QRResult attestation) //Get token information - assume attestation is based on NFT //TODO: First validate Attestation tokensService.update(attestation.getAddress(), attestation.chainId, ContractType.ERC721) + .flatMap(tInfo -> getTokensService().storeTokenInfoDirect(getWallet(), tInfo, ContractType.ERC721)) .flatMap(tInfo -> storeAttestation(attestation, tInfo)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -674,7 +675,6 @@ public void importEASAttestation(QRResult qrAttn) EasAttestation easAttn = new Gson().fromJson(qrAttn.functionDetail, EasAttestation.class); //validation UID: - storeAttestation(easAttn) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -711,4 +711,22 @@ private void completeImport(EasAttestation attestation, Attestation tokenAttn) updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); }*/ } + + public void removeAttestation(Token token) + { + try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue())) + { + realm.executeTransactionAsync(r -> { + String key = ((Attestation)token).getDatabaseKey(); + RealmAttestation realmAttn = r.where(RealmAttestation.class) + .equalTo("address", key) + .findFirst(); + + if (realmAttn != null) + { + realmAttn.deleteFromRealm(); + } + }); + } + } } diff --git a/app/src/main/res/drawable/zero_one_block.png b/app/src/main/res/drawable/zero_one_block.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd9d09bcdb2f2f3b9c3738714fcbe6e84f6f240 GIT binary patch literal 276998 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 &wn%Qo77h0kKU_n65+Dv{>!`LuJ>8n*tAmW$=U zSZ$TNwt*QHPeHjWDAlu98o&9$_!hZhsieGk?N;h7HbgCmLb|wg)e5$im6i9yLF2$p z*uEYlqiN0JcT>}<`bXMA{E@^3sRLAz-C=9$erh9!Sf+jHdK(vCz3*Gyb zojR?&b2|9;ZW1pPBa}6i2Q3PN1P-h6bvhq`hJwUHrL*bD5g4>&Pr5YtXH6l^Mf`{!GEx7@FOlaUB>)^qF zxE+xKl2e7k$r54Ee}fSD^*`z!e&soI9BL_uw$^s5+qyk738M^AaD_u{eXLFG%xIcH zWdmPP@#iP)%aPLTLiXXqr5t1S&M9^afhZir@UR{Plgs z`&;RJN*&6HokU1i*m`UP4Qi877&}}!5^;eZ)E3a_Af7hvd?$7NmiNJZ$ToTxE-s{O zQSdFWNpxa6{w+eL4zx`T_L(yv5GJRHm)im-_vjx!*LR-;Hm7s#d?-z*A_0m&m@?Q0 z^o=f=OA$cW3pFK>O+rnT=&_LEc-@v4PvM8V_FIQzpML`5r^4W#w9BnWBeb3i`A}O9nJ^t9YD2A%*xZpC z8ipC4{Rm$?H$Avuxc&)rj)Q!YYWIiYip9{ij!u$pt%E*&pnESmG$LOE(?;tLT!KAE z68FvpP9C)$z6BA=U?I4SQcf%Gh<-WWoH0_DS`4j$r}8Nn$P(v~yJYzlyh5#tdIUQ; z4ZM^MW!``Io$=mNNEcasR!dnCUsNnblKOy$$Vx(~QkdL)i4UR9;{?9MHqRdp(X+4 z)!+&m-`MQ>>krarCoE^K!HTsobRhI?fkEA2U@vH@g)TKvUkyVBz{k5__)r)$9OiW> zK6}f2<&^&NDb@Wm&~FT?rbDk(hGlcQT$ilSVfa&4r2UxoBakgn2s*!06LA|d399&# z2w|JSjmsg7CX%O|bQFSJAIabT1S=PTg?G=L<6zmbf|9p*0-QGlJz65`SBuGVFuRyR z;c0yg?UYIl@sMrdOu3ny0h&gdF+liKR;J&)7IL+kNI4zAAGb(%j;g->9)?arL+X5G zo3gAFjf|8Hk}*Q{uC7ZjSq2^t8GSM|5<-9Qq0!;N88BaYs(SFP`t%9UnU7)OFx&L0 z(NErq?AQ#oHPXlt&V`*YpbxY(LeHL1-2hefCC`4Syl^gl=2Pjw+x@=z2|Nv|?mg^_ z$3bZZ^39O;Loh@d%bW|CBdPCx3twI{e|^S#@Bj=M1HIaDywIZxnk%74D~uWrefq;Y z>)_SR(4#kWsfGnp#kXEd-nyeXdRl+tJebqcgz5Te<4DsH1c_#fzfnd|+>vzoR*)Q? z(iO{nlgImeclV<%b!&A#zV83UkGff_{WE9Amd}HLJ=MJnUr4RtB*Ep%FJDwVc|v5& z(8#oLy3xaI9n5tFtS5L&hxfVNj%zEfBf9!Da<7a@_rnFgX@9E#P%DQNdZ}vFbgdytw zJ&awOjP2FAp8eDFHp3gA$`9}GxAwtT$L%k#CH5aQ-ndZw z;ls*nciOIBFaGgq=7--&r_aIeo%J_vYL>q#BxqiM8G?d2BnkA+9AO;VSL#*|Diu=3 z#P9$|lbv*vLSqUxRcljTrsp8m78U*Zx4|#Jg^F^G+n_+x>9A`vIa^s4bj(qU8f_$| zuTDY0Y6&L68c1wiqRs!D=qAscpZ@*7s81g=T|FGUdpUOQhVQGdn!f+0h6r!G>-pjP%$1Y+ z9zANdY!&NitDnMX&vfgh_R|zsuycC(y5&NzHe>gu?8t$MnWMy+Lp|fimd%`P+PW5Y zzaF~(820b=U%wnXu|IxpZ~4;;(aXofdp~YCb2xcuzv;@E#4o>2-MdqH?o#B+X}EG& zcz)Gz=R)AxF;HO0pf1f9+lsxs)5Cc}CV!Z#&RgE2$qT$pPFuJ%qx9P3kcam>X>V^j z5?#_!G-IrzZ#QfAt`LtV&R+8T>5tma-mwnvS+#aG4HlVc(IqOXl(8hZjJXY~2QFS6 z?A_Pd(&XsgVDHmpYHf72b&GV)$iKcp;iZ`}mT6A50DP|Ek9HMJpHne@mSglt?T8-w zudisod+FJ{BQ&U|Z}SE)8o_D^59%(M$P%H6?Ye1WaOT8l&mQKsYEx4~Y{bw&=Rj>u zab!-Xu;ESn$t#)*$5oei>p$Gq|MA_yzx#3Euf8cdeWdNt?ZLnJy6X1j*r}r>moF6^ zJK6Q&v!d_6uX%JQe&j>%=eufl9{_z;*Vwgg%hKX$lid@i+vZIQ%$)3M@8RiEmF(V_ z7&%pZWhXp7CpWC;&hvYt*N$ecohrS0Hht=(_P3AiUq8%TyBxf9 zz;NZT{=#w1k;B2Ox44dlLbeO^9agb%ZOM>Pk(!1?W4BOCtEXGFty_t!d-v29N*9(P z7z<1ro18yA+d0QSb6(Z=FY|x>Rp$LqxZb@C^$lFWC#R!YwhNWbpDDOhvT9C8(S-4l ziIa-w&d<-D=N~<=>xPB;o^9apK(}V^#l6n^$6^nzMoynsUOuh7d`fxhxcbC#%keYn z14rQLP40&$>T~CU*RQw_A2FXj?!J1`v~(2&qmU>8QO8x)WQL6^nK#=udt7kI+~n+; z$&MMh&Uw1Q!#~htf!8pjva7p-Ui_uWNKM~6Ut9OYeH{zQAi+A987bw zq)wVxzE1F2b?6z`CD$ce{^9%h7td5I4US?)B~|G+lq4bMCMzR=x3zHUF9bL=aLTXl11u$A2s~zZz^t|uuU1SZ|H(7)j8ufa?x1> z0zJH(7TwSu#nUEL&YW5?ethNp4!L7ke8IHzh%u^ueIV&G?b@1tcEx+)kn7N=q1$Jq zr!4s2HL#~k8vLVe93R$ru zBA&t*@NJ-l6}-YlX$b)iO;T_oL#e)o1~6;=qXv}DnVy+3(>rxW$@Lp0|M^d@^9OyC zCqatRb@4Kl=cpDIE%$KXG?kAXQ#)ry?Yuc9)2D`~O)_^(?zUpJci2FFU<ZjWa^$GU9pAR5#S9#b$#gi5!0RH@adafe)_KN(S!V>yUvTJ z(_cTWM;G$wq3`DHvah~t{_(fot2cD_&UXLiVgKWY(;c&HU3x^ynpM6a=kt_|A03%9 z)jefIV&mfAijMT?VNG+U$A=CBty)|>Irj2=^zv!l!2{}Zr&VW83MUV9x6X05F39+L z?|%61p?vSA0xv!@0-W+6t>16BB*7V7_7J`Qd5Ge z3Oe=O>ZVhAjrd!m?9)WYLLs>@6lt!~zP;J>>bAO*7nMi$Xbya4JF?$-=wNjJk-+YK zw##Q7H!g-gJ5YJyx@+eDc9K*5mt)pT4htZJT<< zQq$ZykjryMD_@+aX*OP^#xNgM6^A6Ll)Vsgn=PeTP_iU1)MtwRaG;ViT5Pli!K9Q+ zV)&j~#sJLid}F)eja`Z`3O6l<5WGMDTaqd4l0fp<8YxGc&M;CiyJFFDWksA!agGXs zXdn+g`orp_o;~}@K0m3xe$jC3q-Ea`%b7FwJJ(f5_6JU!3>`VCJ9rR|?lm4e2pG_2 zE75#O26L#ig7UQ*vS2fT+Xi+wxP#!MQnt#vDhS0nThJ04>>g3B3oSRI{ z&%vAmLM+jUCP!UPaCs?2{b;B8omaT#W}0u&nRS5@&g6q)M0qxZ>U7#1e%T>Y)=NzT z1hU}v!nCf0v@wbike(`tx`nN4Y&F{q(-?@DanC@778N`nRTMMkvb@Iv z`N;oI)L($fbtLJ+aMXc1#mvmiRbAE0r0$l~l3LiJ7Be%WWt5p2C5zDli@^e0GLFag z@DA+GGTgg+?{mNJ`TvO1-us>Dk-MwT$&(ow@J41tr0<27Y|p)D{osAa2OqfJf8TWR zgzmy|{Tpw&-hLn5G5Kq+r4OGp9QjcF%CpS24KO$!VnGOpS-XSx2dSQr#YGigk$<#G zNCshSBgiE3;bZ$rNs4nrd;>a4^9(saNlt-OtEc?7uv_7xWpj#q5Kwz40XYxJr!Z0} zeu)u%Go~twGpz&-9fTs)gDF638|Znq6(JviR0I-nD%ax(Kq5xx@Mzu8+fU=PTExP> ztRmzJI^3kN4dQws2nt_ZNLK-)#TT)4RH{w%VwJRXZP7@Y8^G?M#2&jH!q{v8!anHf zrjri+aR|o2Utr6|^K*Ac<7fFr1)}h(RdGhs2Yt zRpX`eS}b~5m6$LK3JSs0IoV7jV{~9w5jfH!3dfidNx75GmJ;MrXJZ{UFK*VLKW_$B zX0EB@c_V@ZOQyu2_0TB6VFDYJXma~RKJaUi(B*V3?5GmBwP-=r(+$%@s6s#avAeMxDp6Y8MRm2;8r4rCVbVzU! zi?OUCmthSisz<0KyP}d<@a;OMi?e8Pz9<-kK|RTM#BvWfSrR$6g|nxm>4-$cZE1R? z0$<1IAQ^#x8Cyq2AEQj=F^fZNfrJNC`3rCLnF6>lz zNEc9K123Z;gt}_65v_9%WasQe(D0te%*v=HH2xR+p$3bNR$`E*jDc{F!XhLpLMTpX7WqYr&{t#j#q@U41?Y&f zHE3-_MZl?Oby`qq>l#Tg8LcdnRiG9?cx=EwWjyHla)EOPXlqujWoxQw(O#niwVjrQ z^$t}Ettg5zBc>K1_2~tcnhJ@u#=t6U=qr=y)|=^ds0(xoM^bcWIKs;~Fw|D04(KDW z#ml7PxscC!$nLt-eTiqbV1X+B2>z#7b8(s86qWT9|EFcPx@ zBWEe|MGI0BU}rpO+Qg+LSvloU7m_qOEoyL{jwe9#9*yu8{F{?98fUSB|HUqm4kf)~ zqDpquL5VpY<&huIz!L3I{#kRSNQ>EW3Dx#Q?&44wHKgmkIFCpuL_O3@qN;*=T=?%N zL9NjiOB}kZ8Vj9VAs|#hYry)05tWN7smgH($R1L;xuyZ<$$&ls2?$yk1Ef-y=0Za;_sd|80l)E zO(Gs#^)mOwH}SedG;4GK|Y3tB!CU&uy0Dxa9A$Ve&V z!)DOVm?G=$R*oK}9NlB+?&ABpv~#9#IH`KnqsX&LBOPra_k=2RQoT2zZR&K58msE) zR*V`6M8z$i`62w_x@Zt+a2 zu`M7=A(AJd2$ahbUamwXiX1_4J5*klPZfGK6e>jMgmo!y0LMiU z`clx_)DuR5*=|o~c>#?eT}Uqc5m3shKC%*WL73Ucj~k)t?t_5|Fma50;Z*rRCpZv} z;wvbq!RCzFo#M{0j)ndfTW61>r_VcaqN=-_>*=&Fp360N5MHfHD$MA~6Jf>uUzSNZ zBy}0th#G!Ut7gd<7+K*)Haf=i3Qcw3cd43dakw}LTDcO-)5>rTlPMxHRftjWmVmsu zkvJaJ8zTpuU_q!)+ELtB{O>4Ax{!_Q>Zy!EN2|ta@2G3erBeR744ssTEgSTPkOOa~ z40FV~_=4IpVaRwyprC1*CS6TZuv6!OVKuD6z@u#xARmJIChhpKlIi`j{$3c@!>pPO zW4ocV6~cZn7?5kz-mKJ#8Vl8v%2cXC1A~kvRfy0lo-{5I_XUxqD@CbVOj|`IDMv1n zKr8^rc!dld)!pAmeq!GkVa+O-FiBc!hhRdCew4=YMX5kO5e^akKWs`@$nz8c!s!~D zxu$C3Y!sp$3<(|wLIqkJ z4!NO}@N{%($Mk4Mj)MNtuz9X@L@!q;fyF)y1CfH_`!o&n7Wqewv3K2BscdSzhvm0If0PJ$K@DJ8P1&#z1(DWstD%6fa4cwVJgStTcTd#9Xn^e1Z zdvf05(DDVr4GROarbm`8ifvkzSijmjehg*K$*7)z5jcY}j)*HdeOB9|)v2jN`Nd1K z%a&Q!F4k;V>|4Jq_tJLhM2h-CTZ6;mq9zl9e1shf`>VFEvd){F-nz1|VMT1_w9tYf z&!&aZtxMGn;>wd)XTYA%AkkQ4M|>JBJdapJYnd>)`Nc{0i<5#G>4Bv|+;wah^>7Tvuzv};vp*}THi73!r+v}>0* z*AAt&E-J5@tr^iEs!X-Tov^t`6HDr1sz=wP7A|zon3`I(+&_P|fBB-&t__jJOKlt0 zz}QxagG?n}7M6?^TM5QD%&Fb7*t&46Z~N@%_WAy0(~`^PMif)5Rrzo1XQ~-DZQ#YH>D(vkGS3@ywy2M86`2Gt`c;m`mNG5aO5VG2e%Fr8p-P?I z>}DBhw7FK5C7QS_4vX84WJQNnfm#*yWibH_rSS53rTsf7j13vQ$|o`Khzan1GN%%f zE5%S>Y5mIdiiNrrGeS$|nHJBn?p$lyx+eL^F2kzDRQv}tnLrg-5Ufk2tX5N9MY&$6 zsBm17NPhFC=zcLE8o*my7SI^ud`Z#4LS@IJqplv`)3jq-X3JLFmNm{TYrQL03d)DJs2EGYyQROmk{71~0ndDHsXmZhRi14Ycs+e8u~X?S$^ zw6+E@QU(X)HY16IcsBULZq>@+oCP#8bPg9jX^IDAF556O1sBdJY+hEsa*1o+r0lxo zrbTlkyO-xztu1ZcW%!o|F3ihCM`k1OU%Vd!x)uP28bm6q<+={0X zESPNDxg1PZlW2CpqTzi(XWz)+liQg@34&gsw#fLy6iR_GzBZNFztOaGJh+tL*YQE4 zsT3!6Cz?n^lOR#llmg2ZCZ5~}1;Pb1AOGJak*jr{m>6h5%QNU{^XA&7HnDcB~qg}U9gdQk89Low)a$+U!hB`XIMXiYTjINn8x90grNM}(I z0;OJU@%rfqJw$LU1=ZpZ#Hpn{9otjlJ4*37l--#`IlW}U1t$yzB-0tt&kBGz?-*#0&4Nh2wFQK^<`?O<>~ zA_eH@M?-<9cS**yOD&#cx(?gLmN`bwBPoLDXURdYc1{=<*|AOa1f2_pmnPs<`J9Xy zfv&iUVKpk->zkh3p-yIKk-{JYy9PU^E1?%1W&M%b9b2L$loM9)+6_o*#Q1tWs08qu z=@1IDrg6)3#8=6| zMUROVtI;6nqApI{jN}XX@Rk*l3N~(`uovVak6SF*r-59hZvPV?F4mZ(yh>!g=-{-) z?;dQPzf2ZNQCtxETxwL}GURPfU(c>jPPq$Y+Zro%GOYqka%Vm%hKgut6uDGn&t~I> z<)%qvh6C}D+$4b}!Bog{DiPpN{1pRU3uL2wBGI;TjbZ&F=xo;KD++Sh@%sOSK%p}_ z(7i&FslZ{$QPBr?t#+=QCGV!_CR?U}Hc*ca7O{&?o!+w1-9H^_>g-FG&^yoxA8y5M zQCm?!XzoL!M)_t>L(Q$MZj#hiBuzzGGU?4Wa|Ry>n!>uZs`@n2kEdvTB#~CPex`M* z7`%n1jJ$hFq!B?B3pEA%Q=7pPprCqAO&s9z=hv?n<_*ch4XR2FmyC(gU^2p@N){Sp zxu@1@#?(P|R8=1bCz@4puEgw3iQn0>ZM}Tl2!7TCOA7luX#<^{YteDc;?br%sXQ96>s2J2H zHmiL^dvNzE7||@t6t$@;@hy;jZYwkvAQ&@N>V$OKR<6<2*3*JEs_10W_4E!Xx|D4- zs$@Ens*7b?JdbXMT!vmKMHkLstkkRO8l<%i5KXY-M)~$`qVk~%GrkyyDiwJ`nTp|< zgqFI2J&%CH=H4=&Rw=15^A%@~yDx1z|M50lIIze=hPU@`Rw*^|uw#Qd0%C)Xa6~+9BOZ6ftM3ZE< z=APX^yhtCjI{FxUYtq*?NE+h zxNS#Z>O2_J#&2Is!=Wr(^uOtiTEC64lN1L!6zDADk08YnQRiev*M>E! zcCxYf2(bb{WZs;{4O^_0ret-C&f;MOG0BGI4DNuxST%U~EnRc1f|b|iib^qw%NF)J zi+Q1(g^o(@kuAD`k`ZOT+8g8dCNEYt-EdI5s_c2dO~>=*j;bg-MC zB*DeEP;7Z|??HiU23BB#uJZc>=`Ug9LZ>5D% zT8|aeY{05(*uE(+dIEmyU$dC6s-}%qK~5{!;({T~CC;~0$h@`}?0WnYjW{i4NEkW8 zh*^LPA_do;wNOCGRERoD4n}$6@eN{CAM)$bbrg7%Mg`q~52^I|-CHC+Cw)tnRSR-w zwx87PukX{K{{-GSKJl@#TMt|vB5O|j%3@e(c@t=qm{g@{8XxSn1M zR6GiYOH1fv)g>r9=mH_dhhNP|%BiNuwn4H8I;~^H;=t&!yv&9Gz-97h%XK21I9lyR zYANwKG=QO|XLr%ImQtR6>q&8qU54#*Jk>=(Y=Gm(%bm^mS4=0;;RFaE;9TLu$Za?f zP9ZcGy}MS4s$V=?03VRb$nG%`Q)^B0)^(_ACE+;tx>#5RFT`TlWrQR+3K_|UyKjVN z?o{j*s%27!!w30cLn6}GPj7WBqnKiGF$^CONQ-Hx|CR|o^_!N1M#?9>uDu)R9j>&y2bQ|5e*>(hlWZ+3xPFp<*aM-I{Jc2#VL&vDr-$$7h_Jm%jZ$3C3dV_ohwy^ zJz~Bu+Hj;ZtJV)qmd&LC4J=Iov4eTuEZdYpO|iPr(2+@1Rm!!AWHwVO=aPj?xz_vC zI<~7;;Q6q_L!w3M5S=3_sHL`z^R2U{qA694>a)(6j?;t21m{=;CnCe+|FKu1zXk?r zZq0(wgaO(YnlsigG+w*|dxfMVmx(2hj6?}E2tUyztE<;6^A1veU~+JpYqaPnA;fSG z>zcmS*y=e{YaGek+-g}ip9nyis3fyANrX;`0h(8>RF(6zC5v{9Sb|ci5IxIiVM~>m zF~dHBR3py6hT`|YiMpUHE@`3?#rYLwlzySvkXcsE(-6dxFj6XRPxn8u1u_u{;uQhS zFfs-8oz6nTe^@9lnc=9=?mdl`ETuS49CS^Z&@pEgtpIBj+W9k>HiA7;mg9Mz#f#^} z#8vX;=m(g}swJdEDKcIboHyHCgA+`vZKT0e2xZa3VrgNV6?`vj0lk2vMOmlpNSbzu zih>jfLi%=ZfV{Y$PXWJDu<4RdZbpDZw4*cEQSqQ;AQgF9Tb8SA@Hg3n#Id9kVobuMy)I=F0(Z%iMjGHXB@wFknPG~Gori6g*= zK@xU#?OV^b6iK-Vn&?y8L_wt0a2%J03=R&B=Ff#r*(yZfViW_-9!>@;@TfXbf>@yt z4{Te8t`<*{ir+KTVf-UnC~K(To#Ac~>q`cXr9c$y~m^rchgzFBeMXN=-v0TM5Ur+=_{c zNwgLq?sy^ZXmBWa1$sMTTbF{zjxX1Z8f}|JZ6Y~Pz=mP9Evr^35yPOmp|nUG0=|p3S%#Ooty9L@ z26_?Hp)3(9MA?31xlGL1B}afxB1RKKi*h2hz&w47zK`O9f9^8_z85w?){PB{*_f&r!fs=^@#s-G}#Sg@P z+h-05Uer)%;k4=b^KAu`6P97nEB(0}w0MS^g3|_G+W~elSB!R|h@)t%C^N{opiS)B zOc@#UfIw-mR&(NoU35GSz8FP-wuA9U>_0Z4R#fd=C)o6)8}NT96;_k;g;ijtt^Q$m z2FJ^w0;J_9)X>_7$5w&^zXikwQmI6OMi1G-YA?;43SKugqgHtKui^Bl3x-pPXdX~+ z<9#?iW)aRQjjGa&J`!hoSe{)y%QdE*H|X*9fXN(4`bfe=*5bq*C#A=poHh>C2~B}u zj6b#o)e|SdZsWHorbq}ey&Ji+xyHI;Dn7>{u0b{-ZbO}|5-R~p`FJ3-ZMhi1MFnZZ zY*re&(-Vn~>LkU2ebolsFA**f2aFSrmPN#XNF2o2;|5$4#?!)$TrTP&ghr@jk*N4~ zPF7nCtXxIhBtf4DKSWNTr!2Wlw3mua<9PT)*odAEeTCSV3R$5k#a>dZ5xJ>!xvSDX zu^>8rCacTVt)W^*0_`k|^rV$!oF8hxuyP6HQz=i@LucwiIpmGlNutm);J>D6lQgpi zaq5u*lnN;k3TrmW!#PxAaZjqVx~-;M6O1PlhzqjSXeU%NM>;5I3cEo}jxV5&fuyRt zN0mzqGq!$2k8vsL0*+yLmZD$8p%5j9$BVq8bT_qFtE)wt2+F~U*6Gu!8Tyib&;-+e zMfwh3HGN$?dKvgAaGAcbCY%f8b~JA!!T6Ysj$V^W5sibgt~R)k>WE{{@JFN>&JFSb zaZ}@(RjR5C&e?F?FQOkR73vwWk!@(eIWQ7`XQgJDg4WW!&utKr0dd-C=MyKyC@#+u zTY)OLV7hHmH{OOf5OX1Dd47!~ftHh8eVhz@C6O7SJB|r;!O|g9XA=@%N@fDNLe8s@ zktG5O1Im#Sk(As&d!DwHfE;ZUiCv@8q{v{@fGEP)7~*h{D`g}?`9%w?6~rctLaIk- zE<^Fb(NNkc_E8;peK&1D<1mM12^-PM3CQViblTLW)e!dK?TEx^FWO*Dzp#&BDmH;w zR{Y}U{^K8NWq$W+rHAzQuuTHHHuTC`q&z-A6TtwT!MHZq%csBt2nJ#=p>ZQt(jZ8rZt5GFwPctP1uICw;&UoNO66x|GDfao)s)Xq z0bok8MuS2#X}~cc8giBB6NyjYe??9lwx^&lBPsA@)Q!-n(Plc#8h@n{Jmh4d+Q4tL zEP-KJOI=_UxnXFF)a3af5m59@gk2F-tX-)rC-Dsmak_}hiKO^Ld>WcR)jI(v4?S}` zdA!)d3oH2ywuDcJ@tdN|6RYDBz?-H{){P<+E--?)^$E4RmvRm*k%}Jpzjxw4^ebW; zV>9XkbV*dAmK3onjuIgvYHk+QD;u9TM^_{6De?$skWY{UaOTCfhFhd65y(C00^*3+ z@bbCxas_Y0{vlY`k`+P&f{kKNmF{P^h*doit$~cVPebB{ zsKi!${87=@MPNkO1>YsAWc)v>K6QTQTA9y?y`axf)?DV;lS{BmZjv9;Tt?z)bZXJ6 z@Dh_IzjqOY2#^qb!&Z^yscZzYuTmmtrHlatng8kaN|gmaq;&%P4>nD$5BHx_vwDFh zPC`rZXnRar>(ZH)spEJTsTYgOmv1Cm=@safyZ`8V^R(&88uCFIg~ z&_m=9^g*zLID;~Yoa(j;E@xo>Uc3e8k9eG6^jZ@qZX*1g5sc_hp%?T0v5!34ln!p9 zHnBB5Z7QLZ()q1AI!6e?%jZB-t|n3?)zO5+BH>_lf~ZZX0W9PCbPL6L2gDwPVw}sS zXle*S3@?MwoQN!ew897U(b2lF4c1y2@wpz8mAUWL}iFv@fwyFg!0%FfM?be z^FSvJmqltYppkN@_H{TLqW=t|o4C#M5qjah#34!*MGBjploJk6+OR@ViQ#xCo({)_ zN{$MD5`*Bs?wJF&{!x?|6Ud*#;vUD4)z1glrH(9`HJ8Gcw|WP?d7adg-+B zggF_Rf#?Jc+gA#BgUU#3LA@LPKk^eXVsP$|w7VIUs9dz*^w3elh<0KD0_q+&WDAf= zTGy;lPn}9Z*9hd?0on^hovKkwY(|yOMMfKS0n01MCuc!xbgo^fYH0$iMP_s932un= zBI0R5W%a%P6zFUkjfWy8@uhk%<--~r$lEL!vfI`$O=5U5$`1BX<;?DToR&NBZ$uVG zgrqo0_%#v-ztv=SuLnEgTwa1yRjI*dj6J#>859*KvH+kClxhiaxZR|u1x0Sx3ZNtT zv2h9b?cmZYEf%tFemAl*$~VXr-sd+6V$A~N1`biW0>`VOa8zFe{4p?lz@Ma}1YH4$ zr68YCZd<2Wxq``*!03`BGP<@_(u1r@XS7PSdK@~jIYr}h6vt+FYo6N(&Je4!2pSw0 z=t)YE)i^Ne%p;l{Yi5JetRxpnlvLE{CS)wSMVWDCcg?}h61t=nAS4O{dNuEHQWUmY z#_7$BJCxc(^$X}?B@&%lVY5lJ1`6lZ7y<(mY=cx&hlqn0`m`1^MBG$Y8l@xcN^B)O z&#|18Q%D7sLTAGXCEblPE`oe*IOy2Dj$V&gPHm&FC}|&O)&RZ~1d9;NP-&TT9*WiA z48!z(>66=l&bIM^&PzwCb9#<4B9zGCUL0&!G4IdP$r>Pag(9BD3g~5w2BljtjcQlS z69aS9Y6bS+?r?5gAaA0qXBq&i8k9}sFEb)YWM(kUo9!Du(3unm`H1KVs3lT58x3&* z={N{Av>H~=rW+(YoAODlNErp#oK!T#WCVu;{85OaWW^z#0;5^Bc)Dfr9I&{-8v&;u zjntq6ULtdvY3&(X7j?UB#sm|^R`Dd@V8lM63PiPq$kU+TvklI_8T{1^ zX{Fwntn#Gtj(pab%Nf&obG`CWx+d#6(G9@CvVX8-u0|AUX*M-G@jJ+41;((>Is<%h4qm{C+f zfmjKu9@!-?S3_Mh&K*u5F_mbKhCO-JoXN0p2!;mvg)0K@eC$4W$a3D)J z&Xhm9YdU+@^2J@(7q?AEJ~V&urt|IBJ^P>ZY}yEo&5+7iYn!y8gc^keFM(w_xgwo) zubk@~I~p=cYjuscP{er`IT>`m00d(Cqx<<{U8L9ng<7cZH9hi_<=MTir#4e=IhZV4 z7Qx6obqy6is!@|G{LIs0L?8=hgEAFVd5JhbuY)V~$=#cc<9dvRlC4;;F4dU3M+BeP zgs_*y8y~5Udfs_abMSTdYtKQd0)nHwZ3mqP2MQPK=FlfBMcsvTER$1KWlXUQEuSg` z`-|%tjkv&U08v1$zxA-G8Y*-#W-POJr}M;N+u?(nS6+6#{Z{QKXEPsvoIid_`R1F2 zFYeWT_J!rS*CVgJpFMb}^!~fr8MB#4iLGSSEk&i)KoXBMF!0uFPFJq@-+e-#s#OGn zs*D@EsYEq_T;p>nBR_YIXUnG8RPpX;iDU ziD2L{DwN7|Ms>OX5kG|ey49<^Z@wNldff2po4&K>RPViSKYcoN{&MQwBi!}Vv0pz_ zz5J5n)mP(hy{q1}LAz{@Vf08kEE`#3%~WVjmZJrAIj4(Z~9jNX%`I_E5BArOk}eq1wls;s7iwwYwSf8COn*PqQj^$2BcKhb8J*Y)_r(bBSg0M$DYoPPB=dP1wm07Ke1Fe>=bGuQcf6;LnqPfUxqY)@{v2p-fP7I~EW4{}3mqe^z9=bf zROxaEjO?*@b%4&v=W?P)1TqbahEAgxoZ`HB4t}`m{?h&%eq^d*? zWXkx)|qX+Clde)<|d|J;50 zy8FO+=IlAisZ)w$Cv@k}33so_u3b|8bYJ)F4ey0BipL)p%jYr38M4ME=o*c4`Liw% zA(t50BEYhFaPqbA-P8Vy=MBfsavvRJ&YXaY$5an)z?q}$!H=1vesNG1I59E{w0DEs87|H?rKdJ(T%cCSzmc9Pa7Z=C~+BmBa%l=*|wZ*w^6 zOU6`y1OUM~qXw-m{MZ(zoTktb1gQ99apqoq8Z=0J6*#@lgD(nKPRhQ$?!9py(iM=J zD_eGwfdYk96PF+>8^r}o#1KlUF_D&2WgDsWg|%Wn0t=P^v`_WE@owbCCBx;@lEd%8 z!M9wuuSRZOja<9f{HyP3AAZGt@Sf$$z(z45~?toF^F-h1qQdo+=+t*#yD zOSIR~o;gyCVbID27cS6{kD#D4@`i?rchAXgpMkF~!^%bA&oph_Gi%3Fj##FB;+RTL z57~U2M06p{*7hlrQqA?zTs%F{+y3Dzz^W|Pn5MM{YOA8BKMb8a${&1Rdg8F*%u(*x zM~Vw4-QRq!JA2yp!Ev~ELHpeu<&lp>;PxvStRfCG&}b zJeLB!)$9oQ>f5YMt+}?I_A!%d)-1_9y2@H#su`LP=&S~-F0o}Vc#C{x6j(#DnM31l z+^oNHY1SYARx@V>IO2xATje8qNh0Yy&f%RIaD@WRWBOayE{?R8a6ip7TYgp91k z76g*9jVovUc&p>Wp~_cx{lEBHdFZJ8?GLqAK81q^?B_l;U%Vv!^#ktCRn^(k%qIs` z$B+8YpUIs+Y1pv|5@~PO=t!|X*VHv?#=HyyE951B?8I^Q5uK(`+St-O?AqY}ZIErv zU%$*8dJn$(%>TRJYkOxvn2u_aYxFdW=vA3aa+lA!c#d)mdH4#0t(?jOC+BJ_5{?(W zeWi54j?7>FhB@(u?b1QlxzolE4;6m(qw@O2)Ws{Mw~k~EpEe#poc-it!_0`Dyv#*Gs2Qdft0G{?mQ^gNx?l2a=y2mmhsK^zCK%==Ie7 z`!HfE3OE{Pu1btVM(@mT0k1i-Xoj^|gNg=uQ0j3(&O*G1{9CF6)@azjhZPa2Wc{4b zZ+{I3Z^NYriX$HrE@&;9_N?YQI{xnp9q2X57xtkSV6wa2;W((}YSab9_UOCv5OsT= zTnkZ;c&n6FVHB#iSDptQ-eWiJddl|sXK?;3cjvm{%rQVc9rRbWtp^?X2;S-jO6e0< z?s(#OP=`V?qaBoj@2x$+a*1LEKML-8-}&GUT>n^g@=fo1uhm_<&~g7(-6yA-Z+;Rw z_0F_E{MP>Ad#P)e`o8=!`{vuB18-&SUCdm*sCnan<*RSOfx1!}&QngYLn5o6Js7KR zw%0YRx$gDHOp%1MXR!9_XEo=}*e-mU`trxN zw~oa-$3QtArL{e&N=EhTNyVsTmKHkh4Q-OvqE2_VFkMZJC*M;V?0Sp6acCggRmr9! zcq@x`Qo<^xkGIlstV+mq`EFj69(e|CT!zz^OnV<=OS#_XpKqPD2>GyW?3hGDL%2wJ zTvnZ}k|<4`Fx8*SFixFuR9nj{FOQfsEgo$GU&a3EMY#SUcj1WslQW^SCsIdVs{Z6q z;qnRlCm$uRe-eD>J=wKmu`jQ--@4R%`E>8?i%qvb(O*5}{`~T^>vR&QWx;IdOeQk9 z2pw3aQi9eU&KQ#UNVL}7GNNkp2KUfpc|$SXQ1R5qpk{Kn>XM^Cfa?Xo%og^kOi?@_I1ZL&o_ zZ*%`IzOkQrA$9rV$Y*Cee)q$uYqzTpp0vLDdj7;GZ5M91j~w;?_@(i}x!~y|?lW&k zPhRpJIc+@rZqxU7LeIUdbSI0RfYoZKOd6NVXERMTpi^3U8pE3wpz1Xi3!*MS-_M>d zA3m=+@{#GxN#?81gTMHVcFcSVHxSnpiPKSe>D4l`O*MA3Zr%)HWvy1_bdZf|s6hrL z@sav6m%fE>zjS?Z*7)XrSh^h21=*JE{2R~1xPBjDTo0!hT+x$BDLigF_!ii^8b`I2f5bkVM-pF7&-52p^eGXvoAt0%(j)iKi-s_I}0c8!>14BN6vuW zA{|%f-?s_!wZNf^EY7CD26nGy>(hXW40~=i^VJzK91B$$2idUq4 z8~FMr=x^7)`e@^&i_z0p;nrCN|7<-JfN4s(~ z44F1hpt9Z{Y;Ua9`+VSWS{|W7rg-Vfaqy@cO3A7^%4?=IyAf@XC!RovVSMES&F%C0 z^KY=1t{X0V6MyD4aHs-nSA?hvE_gC&#$c#wY;J65D@Ajqa5f%DFcf+OY{>0cx5i(o z1X9h`75fZVufc`4EtgKiicK)02l7tJ_U)>N?pl~T4YDP6(bU4ZqwJIk(AR-qd7s#& zzyW@Gr1!zE?2Zn$XM}v^gg{-<+){@ck>bbn2C*`Z5~|DIEo&8pEO<;B2L)&bJ93_$ zCf%gz5GhxE@s{tX#|`=xt%RmFC^y200mx^$Cw9s< ztb@7|bmn3DDpAK|Af2O?1$b2uOGC2V9nCjUd7`(p{I@3Hj+tn7a^ZUiG^^G*|6EP zX#<3!+>SkP`$qik7fi8Eo(e^_FO|-k14$qF?6go+WrBbdk{T zTOSF7K^|K&T~SAg84L(YlSzSK0f8_BZpzjG)5s~-rR(xPeG4bvPu}_*-g*NbJhYv; zBFR>nmrmBrp3FN#AegZ=THZj*H*1<|^$KX{q)H9>9CXw}zzfbWo2_$h-r{&>H)Z4F z%Sf!zm5{*FA$WYN_1hcp^=0Xox8a3X;QWQyrx&1abZGTL?^rR$7R^E`%Ia;yby-!S ziPZ)|a-#!yA^OM$u+pZ0dHkHn?Qb35eFlrBD(mXK`9?6gD4mwm6LADyKV}>>ciI*$ zHcl91@hh7V#8(d6MF$df%~M-|kyekH$XU#S zpYWf=CF4V0vpJG&883ZwGrMoS{pK0=%PZVl`{9%0aQ_xO{kV5h|LB9yImc0DeU4W2 z#mlHsry^RJa%8d)FkA2gnLW@#?jc^BDj5U~Yc{H@stXBni~EMV z=WMU53uwjwN6>^AVTY?M4RcsBTck(%jCS zro%^dXHJyvT+Y0G5RM;|e0f9r_)cA2b^P(|l-`1zAV%30s|xXaT16$#L7GlT#1Xj~ zn{&&@Cz6TCjFrwKm(?F1NW8d@YN#mWP@h9}=`L0a=`8KesLjZ zK$VLm_XQK6*CyvrS9LUs+qt~nZnuQIlr1Z8z6@bb^xOL$eDC_{d)M!OES@+Id)K=E z^81>ve^pw(HnwCC%2{K zOQ5L^?0UUagS^IbNFM>F^`!p#jpoN|&Od#reESXd|t=&E$&#-9HI8<1PU!h zIj}2G*`mS8_UpWePo4rtWROc)*wK^LELC-My z=FMxBM!UbGT^4o-E_D8pf{dn=%QcM?YmH+Wu^RMqke*d-n@okc0AG`kTAH!G9 z`3J{UZCg^DNCky)0O%vorBQ2Bv6wGBrmBG!9&m6Y1Ty1iT04Mr_wIN z+4M$qkM6Dd(^vXm-_HJU+qZ4C{q0lJ{@?#!`0aOHbNZc2W{KOFV9upjt&Wulyi}lD+4b>b)OM}|x32za(#b+J*?H^L-&Rdou51^Xx7{m|@Di4Fsno=FCkVw?cm_K{7 zxUV6>&pF!LGdnD=zgGDDuc}^r13y+x8#(XIr+kkswJ)C=-n`nmeAbAK8%u-JBd9U^ z8$d3Vh64t(3zc*}jD|;Uzi~DG$6tHzT^V)nMt09r%3t2o|JUyu&L8NSJER(ozBaO& z3>aRoP&79+;@CwaAypLfES;|{$<}s+M^M2P``nev6BnY_ubDeriLrqXHkDAtY80ya z&Q4aL8s69RPw5Md72^enFb;L06J|}ZoOmN}_m+L0c*r#mhE~ed9#+p&w{LXMrhUx- zA{2J?2=kS5?1eM#^XDL*N2Dx2{DJmQ-^iv-<()=CcC4IYwNQCE44%3M>?p0kg(Fav z&=+#9LX&dzB%!7NnXr20O8(kszOTQPL<8b#mexqYJW@Int-lpwVO_ZlO=Yf9Fs8D` zLY1ax3>9gh%@(ubop%)XKTW^$Ds@mTz|;X~C@By=6m!s(fd&`k{puZi?6)6U-ai5P zis(Mbu~T@v(O?M%X%|<|Ag3x>XVhBTqBi-F1LdqbmTd)@({%Kh?A9gKqjYvys7x*~ zlGAVX%5@et76%{7zbIeZausz$gQT-wJ)s9;UPZAiPvk*smt=B+)nWq1MxcOl zpk!y>Fy1+xx&3MI&2Iln+E?g5M(vIwuY;lTv^Ob`$%|AdyXlV!lw( zNGVJK7=e?T(<296YM%r2_E6CGtj)*Ypke!Z{qIST`L^2=2hx@aRiTfBIec-S^1$0)xvWrC@9ykLH1~MIh1lc?;538TZi{A$o_tK@cjua5^mw>=iMe|@fB(8_&TMVd zNZIYP@a_5N{+&9bUPq_Mqmm>Z&Ih9z>?gTX_+Hl3+-#~wPgbVW`qVz0cW|!RXu0>m zb?6W%R8e_M4vJ(=3ZFzO78*+Spvbfg=v(sY1SLMBsjG^$fH7CaV}FGz8NJi0?FI&_@Aazn0#VknVtOIO-&KC~S?39*P+d`>2Ye@N)mptuXv zD19s?z~B$D2cyoS;$*Di#;a!Q9@&E{g>!UFEpaeTGsRacsF1!(O(`j8jrKm_xOCh3 z`T_6s&}?uJN0On)M>B05v(f=RR8JA6ZJ8aZ2!svT~g)E=cHu~VW;j8yj- z-2Na{m7(NQwH_&Dj09z3Tp(T1v~B7l{@xY1_=)qvRS3i&7*W<%Iqu%DU%4qYIB9Td zwKZGKQZ;^YkrR8)B9NlY9Lf^Z^H7Qs^<=0(arjb=FJy{`kPE4V4rh|gn(O$72b$9# zq~CfQ6v$^P$*je2?Q;6kC5VYx(sG4_0=mKA_Bf*n>MAepkPA{-Cdp9c7=*Hj zbq~%lqX!!LCd!Ip_E*%ePZd(d7ImrY>}W$7b2bzh8I_I!R#HEGHl*sD>mTv{>Kl0P z&D8Nznz7RvTdeopGtoO=39Cg@tdh^2wGPQb$3k%^rbsVodSrif*C_f{j^Imh7aP+9 zGt;4}?o3OuwOxX&f}<69tvjYIx5Zz6QTfFg&%I-?b}khi1gJT8*LdSwm^&LyyRy5> zS5=oQ*N&Jt!4ejI3ax_T0Avs!*%i*^8;iBYL|Jf)TgV;xmLI;D_VrgFN0%8H3wc*! z5QCygVQOUIP`qS}2dfB3Bj=a|A)O3I9D#;p*MvQ<#J{zWQ&q=7PKaX zJ>qNWHI?czm6lP97F$bsZ#>)HJZgIH9Cxh34NeQ3KLh7ZdcS)BZB4Axt<5$XZk&Q! zN0Xb@;Oy8cB?*NzQc&v1hE^q-HwsB)ReE>zoO$NDO0KFyucoq^c!i&SM!0^>`_Tsw z@VD2GE>|_yx7L%?5%w|VTM^jOmaUMBNv*`d*wE<4F-}Vqf_~-WTjBi4^6?Y0n%Z2) z$mZEAjcPim24_T~ReJ`=4~-{s;5c<6efGLy5GT)pv}74m`s8WZ?W@YRZisqh#XO%) zsq!&x+Uqapl@$DlPLzj|XEZ9x&~o~q)Fvi)f_v9vj!$k|9(h((EUV zRh8Q z^u=|F$>;GTkPNI;W>i^Hxq3bjV-sn0BuTIuEeuukpi9!%pZe@ReDP`W>o2&@PN=C< zIpfkot@iRI-Pun#71fFWjyHOfEQ>5i)aO~Lgpx6Fenl>G>8xdX7yd{nj+IA~u4&1e zA~A6IVfOUI5BDSwk40}@1XrBGHBe%v3<+O-?mBiDY$35emjE21SVSH=Pdeoa#GT=c zF`7_DqQ*)Ug%vSs(4l$Lg5Ku)`l9TMGhBavU8}@l(m&d@_mTRWpTY41ZMPm;X06fIj{v2;>dDu0U;m)qvx{2L%LW&1K+=fJjDyGd z;A&>h3AT1{l@w&1s=8cp_FT)@31D<)W958DcVI%FCKzQ)Ij~zMU-+c){x!Jxp8x56 zI2+Pzl`)&Cy8XHFn_pY@JYx2{i~SQk>9Rtu?(FCqKWE5NOfyk01e}(9YWmXUzC=pr zba`T#mJUkDgNf~#uW$8UzcMUZw99KiKBPG$m}L;;K6f@=rK*T$NdRQiiAsI3zFZwp z2eh@l&R^WrTt5N6gcR69xkeHXNfWtH&p>SEY<^^yyRXaF)y%scE`KB&DAzQOgZ|Od zkDg&K9!}hU=z9U33*Mc~tDQ0Z>5p}nju#iMK!tvbmrfc_NTj)G&b-*t1n|Y7L;<76=#F+@XD4aqm1-p`}n*yt|dP#spiFJ2R zaoxL~cz6ka$GH$>Ryjw3pK|0q{I8lgxcK5*fvcbC7Ow&W9b|)#(!7cy##l70+W|f| zL6WKp0#y1OTShkuZ_~IGJGaB%Jv7}o3kn<60Yn0#IYot`HppsgnS5Sibvt}vMkSsq zh0oee(c{Nd*RSfo|D2mS2u>R~QCF&ERc+>9|HgXp9H%f+5IKsBGb&+u)CH*Nr4)LP zuatq?N?H9eHw5hxhgk@Esc5$n-^+ttfQS#O($(MHR$e>^-+W5tLuE#YWk9LoSFDqM zaXWeavLukD7(}^}oJlFETFz>ay8RG{LOKEH%%gm2wY)C`{)nKDLN=rQ?!58KOVHVq z_h-mrV28Z2P;0H;K**MgEZUS*KG3Ahb#?lc8l zl;#0QsJgjI$Jp$iu_GR)|o{NiHr>DQ4vkv05*kU5gmojvOR>{fR6GK$+V($R_d3bWDFgl;dsiUDnP zwJlSIM3zm(L1As4I7i!B!mcX!@ALoS`<4@@z@&nh3F5I*>o{c~Piw;ldwASrHeCRx zmq`}*U9AVj=nJXU19Kl z`~i#_XJ4{5yKzNi&wS&SMbRG~_<#I;{Ox0~YzS)dVhvV`s#8ihnohKDt&Md<^Salq z$!}Wb+_*BdX=iECeCyl^k$V^QzxkDU(++H!E*z+U9H`VXf`M1cD8U&Z=}SJl(L&`D zh{)tMi$KR-GtzzYO6aHCL{M6{LD|A#NE}s7=}k+VJ+yq1Jbc)C^}0noV;A{Y!AQh% zQiv$=O(@*xE^?^PQ!hOWRT*j&g&nmJsS#GOWHCye?dAoM_!I?9*>rd{RL6F$=juy9 zxfuAVm~912_W382=MK8AU(qdHO3S?XK0UU@$Oq?n|DXTuy>=NL0eq{53L+rbr0U7o zE-444#WH`6d*e#q=9Qs!OERmMiC60U6Pe&=ofpMGrk?xA7yaO=GM z+PO7r7A4m#Zd$w!p4h1W_phbbPHRRCcr_mE8?u3?AzO?W@P)K?NUP<%w7(<2W3{8H z-l?XoO}*ct%TUo;)%(xGudc%8t;%vsV8yK1(!utZ_B1}ZJu+{4e)DqO-i4lxs|?+v zh8wc`P%ypt5@TgqI!W-JuZV5G3hWB#Z(S$phCW_fr z@g;Mb)^3&ueC+x~aN$JLFNp0o8R-o^G%OE z>YhDRUbi8=Y*lpOjIl57RMnD@?JvIykw|B`#^@A}LF2%XOs;zOZT$#Z z9+DHZ?`m@F+N(sM*j0c!J@l3(7mj&;`m=oXe)-HPuB~gpHF&_Dk@4al%lo=hqbV1SzzxfS!V(7@t-(uxqZtrhmOHU z%S}^7l*GF^1(j{$l!*V(BPy!1&=iwS8lPUd$cRH+M3>fyD(>wJeE(*nx^S4bMNif{`7_F*(bA;Ck!5ceb|taZK7=& zR`4bU9(^h_W&o-))&&EBY2)PaG+SK<9cjtP=F0jN2zk1Lt;Xu6uKQ>0r(d;iUXR1J zfyu~HkahLKsjlCA&Heo!Vd0|Yc!oFy@dnE=5{XiyN)?O4+BjWR?TW{6CaqKY!fO|L zRxX!CQnDSJ*lU*xC(bJDbjpF45kMG+6g&b}H7$@&d+J&9hK25VQ+bW4oGufO5raus zSH$+^O}KdsMt8>7E^mK)GZf;GOPHrm@y?h^C2dDk^z-{0wk%~#)K6~SG}oPD*7NT| zE{8gXKqGqKnLzW}b&lWv=>GO6&(iI*;r-u9)A8y;csMRT9bSF>V02_Z1t?s zc6)f!7U0x8tE}I=-E{g~=)gf;bA3AMBjG^VOqObZUMVP7+`oo(^}~<-@U2& z{w8Qq5zxjG*ctok1zrbL8_8&y!iE6gydJ<{ay^ zX)rWZ7}4faOT-dfl?Tw@0Uz5T6EJP(X9vg>hqwu$7$fU_%M2gXYu+)#Y<8pw_M zww|h0i-;HuVgoVNQXxz1*u|rQz;0@!w7-Mao`9PI8Y-s$_+{X`F9V}zo4|;iZqweEbqJucg`7Ke`oT}XQ@o1N*ZZx zl5yG@42(Ln!v> zR#-LDG;SQ4Y#xojgw+)a6Q9`1-8`@U_kV=t%WAWEY#32B%hM*8AeFWjYR%$?jy;i3 z#)7)G+RC~`&|1;0U8RZ?)!TM!Z{5ipJgIZKN#QdBOVKHejwAyoOD?px%zf)!7}w@p zzCfzAxc&L++fcghjlAs%a*vi+9Wpgw;6phAE`zhF0KF?RJlbnWmJ+2 zXE)e(@-T=S0c5J|6iLyzbbNWkM&`ROe82gNea;3tWL0EMyi#oq7Aqw|JI8c&KD7r- z7OKrlbsSWT)~@ZS1`Wf1@N)J)|GnnLH|YT!GNER6>h7JXF=Ma;s#r{$7qKvl^h80{ zu>*bi{m)N1X~GsfFL=teBT&-BX|KwBG$9vwMOeRnPy62{>t96BmIBePOgo0V4*)KVXR z&-M?0aqa)_GBrp`88R{oJQ+=0Z)^Ly4YXlCuGu&^iNdcXbn4CU%95$ES99mK={H|e z8Wvv2-jnKMyVil-f!4`0ftEM?M^5VR+|$y*xO53<_~;#CI=vKCxlMMPa2GMgEvJTYvjp>e5;9Nd>yZ!7(GNQ3)#8z`W^vLoKj~zo~qL_<$Q6VW>n= z3N;9(5J2+aS~lM_b1pR0`v3LsOqVZF3^`pm0vscCK>SPS-9a@7DH(|rKk7T0H_=t* z!0)VVUCql;?@F}RkRo0MjG^hJmqCt~V z;(r|W^6p2$V4^eBanyJbPbf6_csyhLd`;>0b%x8IHXl9#CWkZ^tRaU{%1|i> z8eM*1O2-boUm`al$aUB50r zdzyRoc_>!7f=N9U6hy=v%85}4lwbl-S+Q8z-NY(Qx>QwVaGt^)qm~)UUzKz^$3DAG z*VYZUZv=n;_ioe$IDE-NghEsb88n2y@NXSdm4iDU;iFlb$(5VCRCeTK;od#I z3Fn#=l)1lq$vbb#LVm^QPQ&CL>IZSuia~x9#U7yWkgntO_A?h%4}Z{&B`D6!m?WD$ zoi_80RA&sY;>J}%Yb}mmP`ku=K&7CgqdV{wuRI(0yT5loLx(gH2g*?dq+*sR%a#-M zk8aV>$seGfKE^bDfF@93!pRWVtU=v6Qhn#HS)bl~j8 z^QPb234C?GYBu6(`X&(%m1ANsFDLbGUt?G{2aG1L*coN*NFW6cl}g=| zlCWtgXh}DwI;al@RiQ@=yR;}{yVvSRcY{r9`sAGJum23IR)x!Hny=wYN!x-=*(zGT zLD6cipYr5xTNg2N z+CsV3Ol#4Zug^A9T`s(;t)a&zd&brCp#t2!AN|eW<`!(Er90wZ8iK2#>~K&}Kxz40 z@6b3Z;=~C&kIG&qk!V^vNIc>B$KC(upZxE+!itc;Pe8BnL>(+NMr0O%#iOp>9;I zpQq@c;6H?G*d2+)pgwp)_RV*Sdb&fuV92s^IXG?dQcNkvJk)O4AdQ8Qlonsc9}fHT zDQPM%277@1`KQBw{5#k4uYy*o^gE5=kRzE?;>~`CXUSCK#6D4682!Y4&ER-J?&kan z{Cn8ED%wYyZa;8+_nnyD%g0tvQT0@d=vPQjpXF~MBvSa~w&Uwx+O}>7yWW)w{k-^x zdP;!EmW6@YQ|QpKaN4_hy>UDhz(DDdAc&`%KtDLu@zs^id$+{DpuAzZ9X)xxh0_Ek zOxAaH^1%@Lv%}w-IStFoWR#;0yiN;7ZE5#PgU<_(sC`IE! zrjjwBH&u4-04kMAm)xq8c(kM@hA$Hr-@S+tPoM%`Ey*OoWeh#H4ZSTyU3RfVx^D6M z$S)rHuAhNYgDhPx>|96Z7Y@6%_@AIRyk`$2kZzO?C6>n^PtT5CKqO=Q&E4R)4=PKx zQ2PMkd?cL|&y0(Nsvp})Asc!JU#X6+ToN4DucmSgkVwXxx<`U5;63t=;>y+R)m!;R ztA}+#W3g-%%^ezKk@H4mivhMNi^YwQxc8md!X#(M;8t$x}m4zaRZ%wa!AWltfZrwX|GQCzr8&A?8{+AFO6;Lrw6^rO2QDMZIHZeSgQWAh8s|M8DM5*XkTL z&Zw^$J96yi_2O zId>)3Kf*q3ip=BJ#pB?zNiuHmd%4;U&%-a}pWR^Ie-rZ69my8ZD_!1LuCB3lXi(vB z%FvHi=%r0{x|MU>p`4X5+d*ERuJ(zCN4ggE>LzEvqEH8F6<>ZE`_&(tHf%T8G&Zl( z;BkXqVXH0L#VGO-yEay>S*S~AjQ)tj>(tpi=+8Dcp$iGxS6+1fpMQ-XJt&|D>+u^r z=*P*Xy#E{oh_;dX+M2O_db26z4{1VnO)eI#v?=Gz5o$^bgEqTnz45cJTL0!BnWhfL zsOnrYd(!eXbQmf?bjf^eM+rNx4n@GYV6L`YgVS#l&_}`ls%7tgto-&H#mG_8tie5V znrCPMIIK*>W2z)k;A=OmR+malFe0S#a)(jo)O@{)3#!6t@Otyx)>!-6I5Ez$s=Yej)ddcx{n(q@D_7&!Zw2Ryk*Zp*s-`TU zUbd*A;`cOE69kL6qc3j&1>Ya8F-STc#~uTN zm5W9j7H@%gT2qr6)`zKCGa4S-4Gy<45NR1VMrX6C;t_}-LCA~b$|=qMl^T_;ar9() zV>Rf6=5!7fj2c_^1;HPUwYG*oK3n|be;Bv!Y@0j^^n|69thlcTP(NX$q8QbuBhiMM zWFskhhSTvbyKMiuno_s#k!Rt-ZU3#yiTb?p=enUXSthKE2i1in*bV4;jT$rCJbGkm z!bF+H8&VE5+xZbBEqX6hmw-zxjP}<7#<8YIWPKzJNUDcQjR7 z3~1iD&P^)=Gsa4+Hoe8A3RrcOJQyvNmU^6e?@O-*|K~p^jvdBvDZMUhsY;SBO+4@x zoyfq-N~6X`J3ASL!ENz@%b_b|svEl03ujAPiq=A`xN{erzFhUqAE36+=M9fvywvGS z0mUXjaA>-^uR#-txhoa$ELpA2Afr=oDb@H>=p1K0m0iB2?4JNGQ1^7%`v=M4m4GyA zV!p_iKm6YJ&S9E4F$zjFm%v1Q5JNSuwUQge zASRo%wLm$x?@cA=ELO8`{0ecJaI@nWG0jWls_RbbGoXt37l>xetY$f zX`0Ba449iLf|!6Jv6~bLMEi`K3RugvlgG1*ro(U`8UcR1gLCZJ>iGUf-L;GKCyD{j zvufmKw0pAXvW3vsCTS=l?nahHbBRxKsG>~BphkDq2uZ-pN1WId{FbRooB#bU#=CdH zWR!&rnz-KXq&#G=%Y?yWV^ zX$y?uqvzqsvFN31`AwU#T}A=L)@r04mkB^^6!Kynx3JT=dlpK5f1y<`wV+#@+_fS7 z)?+ABc7G&YTe9l4-G>gTZrwCJL-7Ubc(P*^)fY2WXJsOaPR^Oy@-$^1ptl}GEy+t* zpMzeO^r(B7Eg--568Yzv< z)ugGBni}+E)x}tSWWt1o*>hDEPpDio?1~fjxJu^Bx7a&(xRW14yij&#nZYI`zB3%o z=0z_|qlSE5KXOE1{+xIf?P1A+_VyW5Z1woNKpU0_MO~qww?%RLZuk#>aLk`V{l<%z z>0{woTayiSflS-~_#Ve#Kkad1>*ygALg2GiI%sP!@!DI-fB5_GyC2e1w3W_5ZR7X@ z??WKS+x_XD(WVTo7tqms9Ai$Ua$TdevnTNlDR+H7uwXHqIG?=tOSZm0o2nkOU?EjW zB(kxlz8=%^nXKJw$mBq~Vyi0M$x)#Xk=v{`!+VD#SAVG;xq$N;V9ZF{_^I?c%7#;b zwAa01qYB+}!~zB~XGBuOwm2}K*Z=anu^<2F|L_6@CgbM>FwvW3K+fr^GqjK{*4$H% z>(fy2T*j7eCxbvSh+ys*9l3I)@%CM5t`)neDucWlkW5pxAaNCuWn9jv76nNTrYIRbyrkt?|3Cg&b@v9CO{R2#!k|1eF7RfkfvsQHxh`d z*D92#3r0-R{Pb1kx4(6c9|Q}Ll$Ao`hKs7aK>xSq1o_^k0)vKZC9ZAii z0gm=c1pSdW;OMdBrK`C$=+D!RTcjqX4cc7^+B27QWQkS|hwUjkcciJ*VpBPlv7~Bj zcmCLOV0I!-i6(+MpWn0l5$E?m>G$r(+tl%R`D>)wco!1Egx58m_9Xy`+yMI$GUtK5_ z)l`8Rhfi!`ba*r7Q@I9saZ1xtGA|Yq}C`d38C28v(7u;z_rWV@sGgcOT`n~>GYt_ z=E>&@NFIvjOa|5CI&;hC+apO}q*jk7*WPBS)Kfi0GKx?zTTPCF{=oz1?|$uEHk+)J zl$ACNx2{{X!z4VWZynY{Sif#scW0^ z=35N9foff-wbPdsw=ig$K^u?DJ4g7pEEP&VYc^3ed!hH_8Ny(+bByniBSNe^lGKf!zbgPexvQ21y+S*@;LW&+QUPylnogn=5?>z zDi5Vq%?*MWGeGk|_zu9~wZ8hU>oA||2=#7IoUG_>x`DCKG%!IFk?q=pNevO-l&^zZ+yy>pYQ6&eWlSaStN zkRx5GCqAF$iATu)7tl?RfyE%t#Tcd1ND7GutJ?L%DsY+i|cE%JiHvE94$4XxavnXgUy7f4F8~|__`MT`bVI;5j%*og~|_KVx)Z$IW%;s zzWF@<(;uw^GqpZi9>YO;(~;un2?m87nf>P~x~yc`(G!|zvm750xWdvgN;i7`g5}^F z@X0yXv6H2ZwA`R%Ay%6Py`WLi+OD(<`Hbho@!(5a5i$iMS*w^+%$3qr=U)I(K(4>@ zm7B@PiM6fd>$ezw{7JKq%ACtWzRZSgC>VS)Mn{q$c_#OCK1G!|khliFtB^?G2;_An z()eE?pLB0urZPA)*=EQkK`-kqw+;h4sn>x>L2I>|KRn?7yMN;5Eh*>n%2WaeBu17p zj8s`Sq6Y-Ql#H1YQ4eL~(q{?+u7@va>q}btS)yhMGM{%t6waCpc2;| zjCN-sUCs40`DYJu-e4eGZE+0a0^pq4@aNyd-J6C>r_lfPgaV4O6!7Ad>6#kb2)#;q zuC=1=sg45g5f{p3eC3UeveC`4tE@IxA! zvaQYd^(E717eR~4m{aGx997y>qENcV^|-p4ARN@5|CImyEAu4rs2oHv)g?wCw+6f# zMcRdL6l;mI*6CAV)_AbH5wGA6i2>d`Ww!FOE2ST(I*nWmG!Yn?m@~k6mklY(6MX>GNAnYV)oESAol=o zOh3B|0uBer^qWjxFE1x7EK^k@^Hf#pH z-cVPPJ7~FE*H9Ys!p|AXpp-W@w#DNarc8Tp$U*X4!nnSBc!**9U^P&cm z+ibE($Y!*94;@ec$Ny&LEpDo+C}V_K#H~4eKT~RIW(+z*BxFg%e5ifIOBM=h_H6dJ z^4b;a;o1e?@neQq$Ph!6CzF7pNav%7Pt#(JGnB}11C5qBW7Wm1E}Jroha=*}8zaGM z_u<+__S6B02I7^9Z(_F&2LMpt(Bkrn;f7Ti>p)lEzU^WGb!?vVG?aDKwRGqn4gd|7 z*QWDjHD7*V`J11tOUb^X6+{m?6bi+gsxYETRQ zg^Qa1^jq86(_qiwlaa<8%@;@i1Sd_j_fz~>u&Et{6%(YbG_OkJXMBO0(SPu+_WUKy zgh6nFFnz3b;!OM&$0w-OoHJH6Z;Lur52ZY#mhotiXy=ecc4WrWT@e+;27nY12E4_QkdEh;eJ3(s5qU5+0sJDLJ-Zb07Yga+c zhh(GI;RwauYorDQ9qwN}bp6f0K*!{4w%$&d0WUT_ZbqqjT+CevnmxHhAlcbE^34}P zgH&NH1%J~@%3Vh~**|y>&Y$s|JYTo#agZ=-g}Sb>jTdz2#tR^r3uS~RvSh?7B79z< ztM49(#OD%W!=uZ5-MuxH7OT%GV_DhWN7TRl4gVrLcA_@B-s;$aF+5E)}2s z-m8pL#NJZD=8x33kMQ<)F#%)`PL@yVSIkhVjfrTPawS!g&YA`>E&-G#o674mt2EwY zrxO43f5Ys#ZH-N`0B!OUgVDl+wA|b(QK?Mfpe-8pC;nR(1cKHbo5R71X8Ah)-i_e- zv)X)29z?#N$_LG1#2OlV%5SepM_TF>N{xK5%P`b$Z);TKQ~D^?_`@HmEfK!?ndI(u z{?JQ|=8Y8fzPNt*>7$3OQ;$pUqiLY0_wx#p#xyEpzB1 zMblRg0)P6O(5jVWXedm8^+zM_rh>}v0JXg7(Vf|`6VVCID;Rv&4cEc;O4AdfP{*gJ1tfH)a;N z6oxemBy`>(T^hSnYm3^trs)RfKq3Sd4aE_m_NH7V71$H-<_p3<|71HzYvA+%awwgj z#e&s_}%-KxUdf zUffRHK4N8WKuX4k9k-=V-Byl$*GlEb+}> zBm4SB@ZL52LoUWJpp?_vI7lLkrpt>t6zSnY)D-+gA7g)qfrA80a^ufM3OT-^9Q?<> zSgu}$;X}Sz&=j*MCTOPVZ${d&btfO9#Yglo1g*gxB~q{yeaK2Yo{{_ak{^TJp@wXYG7qzoJCDo3tU1 z6qT_&nQj>bys^Lj*8bOjgNEL;Ki}jOftZ9*mg>A+BjYtqfmqyLOhG#4dU6*{Rj9qvJO22PxVst|i~Bp~z+06VYd@a181Zb)GG>aEp`JRBTWw z^_`%6>UNmvf{lFmuc2Sv|07M=L) zlk!ZCCX0@%VXTQl^O8mSy0R&miO!mAUN~8A^X8K!O*(-C@2aV#xj?=#T6NxlL9f#v zKCJ(zzrvgut)1=Qas15tNKRH-g^bEbJn@(#9P-DA?8weCuh+hLT_9e=&RZnhz7{-p zMwN{i(m${M3{G$NXA{1vVyLZ26`@B|lSFj%p95*@t;>pg*O>QT zvreHC5b8H?04=hF0#%C570Gw@Wu{FT|IA~a4hoRPD^;D-Mx3TdoRyRFjaNA6)&~o= zJ9lG0{3<$+j$okZGG1m1yUle;DpH^p8uo0>%%4q+DQbEqT$1FHnfi9@m+jT(s{ZrO ziI3i;2TAvXvbuTbxmWBpO(0Xp+xz?}wCG4IWEUy;BcM?&IPi|DkO7}xGGT)8i<@;1 zzSPw8m2y@6%O+RSnP|Dly6mBGqpjVv9-%LnfT<(`>(_wILnsohxF(=D_^IsvPqL25 z&{gf*w*~6TV9|j|X$gju3a2{Pme~6^^fptNx>^GkFB&(wA%KWq_0cPa|Mdsu>n}qf zEq7bNBokyhlftIYW*IkPY#f1F3R3IHQ6BM#ID3op|LOw!w5i%J&UtRoIzQT4OWdp0 zqU}OimRlE0G4wQm&X7KOLip;g=JB0)k6N$S)in+8vV&a*wK>M-;td{DptgmJRP)9X z9zf+FK@mlM5gaQws(x`he3jx3QB09wqQxgxO3a(4tuCnwDOya%i}LD4U`lDmcq3{r zr8`W93#rUk6*B+!_r_~iscHaTGESS%N;EQwls4S)s+zZ0+uIK^9j(Bq&DvCf2usI3 zD1%eyWM+!zf1_k`V9Sv%2y9!%b~K1*gb7g2NCy|HzWhRR^d0bqJX7aK#l!Cz3GKNJ z^HhHGw(RT$1jQIN)#ox|%SMpcAQ1BZlS1dp)YcgC356{Rs<{62Mcb`A$wywFxOu+@g^EY0)F9Kl#29KhWt}*T zwvF?J37tq7Ey4|*w=P?Q3L^W;P8NZvO2*r)db8d0=yQ?Z{mJ$22l8a9ux?Ie$>$3Mfy)uYFc0kmvCuPNajlB(J|;Q2@@<@Ec5qF`fJWgc&M z%ldSsMKX7;;`X)F)hnLnM$ScGk+sS?XV#|cYgGZi#N`B~!Ke)=d%8yK-YE~IalW)Q z^^%Z@v~9}>!$PjCU;MhXVzUA25<0PVetPM=G0!~#*$DU) zD2ZIU7GeQ&Z5!!=_g+f>=kE$94$-*ma>nn{6`GnBtSHV~0(OtDZ>+1K6XZs)I1!uz zqN<*{NzXs!ZtDQ6$F=WqIDgD@;A1}3P+h4_j40U5Mn@`QFJ!6)C!6U&Q!vpfY(Bkf z)4Is&)!}(_Vsqxggt_(~{wV*ue;0N?5q@-oX~kUB1={M2HsN-}BD5Z6@>^H0R&HKW zw|1F-)}VRq0{gNV>6Hr=s}{&^9g+RhH{9E=f-PiBDF2u%sCzoGhGzfS+)Q_q7W%2|8uXXfBMpR z>4YpAar#0s3e^%ZoJtU4K4&}?4CnRkM0VL~=QJwSLSK@GC4a*5#M7#8uDRc*aBg(c zMCFNGM(Q^+IMI~NNWE^9bjm))?%)qW174s_4wmIOx7DVmAT>dnE>!>1-x+UQ0h0x% zn32h)UO%M|ibnwBrElq|?0LeoaJgpERKwgM$C{4~THv~(dShbwO2>wIuBoFZ@7(U4JsoCGP|O&x zZdqtsF*EnbI><$_8)je~yk=~39|yc@=IF|!8!a=&x(EAwy&aCX-%>l;|La@tdmg5PX) z`syNN9B&KV+SY>pZ0!H8;B&TYi!^Za+BTa z4+X+u5t$-mxm@|p>+3rEgaz{)5ASz=^FwAN@g6E@ZN_J+Gg!j`Yb<81%G+bvm@B7l zX`24zQ$YL8EEf(tig~HQCGJJ-PFt6sLgWoTaYx)t5~pD-_D4ghByACVlb`0Fl9<~>IBuINy^z{+_NXcKrcMM9lklIymX3An^7Wy zLLo%XK^QGZ_Gc+@l49r;RXMG=Whz&aDAIvCa(nRkXJvOkaePQ6<@Cb=W#s)MBS_36 zT5M(Vh4kyU37=nO&K;m?)aV^Bh>ba!MBLj)J;Sh+!~&eTu>rE;at;~|{1)5eotDFg zH22S$c2bZW@($`4v2l{G%!nQ;E`l}|)sU1T`RSsS4NgX}vDj^iQBBFis95Bga^VlZ z)?YX)akxkq;JaB3Wnl>-@TZ>0piuEHUT&E_&An)@X4VvLXo6zJ94%tMY%Qp)qSrxG z;TfLDYLZbkx6z%E)pKBMhv|t|{XhKZ`0^G^oTe?bqSj()C11i0_e-xq_#kw&=m*AG zmdt~d^I+{F^^~dc*^8jJ-E!+Bd~->;mC}b|MtgHAD< zY|Su`!B5%dPQ{nk_|Hzk)G3fn!RU%%O3%b?n>=%;Nte&iE}o`eI7v3C2hstMA{yZl z7vK%c*b}Y6gt>DU0GGo~&hn>UQnrv$6;06q{-L+K4^F$>pio3>CvsN{7!H*N+>N zKD?#+{vOj!$FZ^!iAgGkao|9-wAn62tB&%8VuJ$_uw|V@6~yQGnjXvT%g#Gz4C3*1 z5(UXDZE^6ZN3dg3l6qR>QsBJVXtNjxM_E-kIUJ*u$nayl3GdEZpK$G@{?;*? z6ba``B)eNjcqArzDJ`B(oym13>UNpd54X!2dFrC%L$o_-H98GCmpm>AZe z$Q0^WwGp&Rkuk&|2K-@hRGb9+g1zC{Os*+HO8D4yPg51mp^v z!QGxK;CDYKv`}p^i8d6eY3bIwLIi7h4G1h}Hiw(2&XvpMYe*J}wk$ucnNH#mCj|S> z$(2I!%y~F@&beW8RXBpYp%u^U94=!~U0cyojR`21YrFw*xdhorfy7};^|fHp$F5&3 z-Mmxt$kQm^=#_?(?U}gfN}v+JH?V9>>yD3TH%7u#?g2YX%w+~Xl6mJt`2J$>x#xj5 zld~hxBr3xGSYIEw!qhjVi6Y`rr3N=x1=axB&CLrtf&jGZ<5yKfvfFY*IZfgz|!OF($BP^qf3 z8N4VDC}YHCepK*Eq##fA2vz8+D;x;$?%y@^(D}9q!Zain^d!zcwj{<52?)#a0|kA? zN@OT}NJ|d18B!W;r-il#HWrg3Jl747>9Q z-OWpu+h--CXeo=f22i|daSSI&4rV%YM~feueD?dF!MWqk58s89c%~&)u;u7SM=S9n zgu(VzU^0L`f_@dSPDnP|Uwcb)?UeVE100=$fWttsMC{Me-l7y+K;NEHp795nLPL%? zen}%z$SfqKDjY90cAmgZ6mY|z->~#D6&%+-c0I8D&2aZZgTetW< zNizN>o-BvR23tYll8RYj_-=Z=Fl&}+PEUYao_MeQ-8nw%%!f`>=b?>rA`ji*wjPWeOe*ysjQxL~#;Z&=l6GB{NI z3_3;l3u-+z+CSt3Ww=nvR~oXhvcu>HnI`ta8jQ)J4d2nLCr`siKgPQR8pnW$J03-`pon>DM3VSO${|tk6X24Z_B-h4Ih6D z?IS^xlnBAbu6~WnDTKUyA?0mpE!T8GG75HlR-k9KXwQwI)lVdjpMZy_H56#WOXMc9 zg*by+!RQMYYJ1DYF1990_8iP!1tZ77_)##n58B$GtrL1iLqiAj z_ripcFk&=xbi$nZfio9vXD*sfpH6-BJ{V0XlmeP*Wz{~II zFJ5$9I^lWqWp-#5j2ocVM)g8h3!RvPPzry?H}-ZyZ!h#rhIy+@Z@*`_d`foxXyUz> z0i_Y=30)oqiZE(%866CZ=s=^Od6A(u;RK}~M4iIosLRQ;xDKUR7TQ_fq3Qm zcktHRk?U9FFFy}+X2Gb@%;*Wm4O{d}*4pQ+a;?}XTe=Kp4MIOoM=eYpEqiWn;?q;| zV;>p5xv85#8@;|vx=>rHE0sNJCmoKDA}vF@AmbuQco`$7%tEr6$U-E63WGVFEap79 znrx#YowmI7dhT!kw)(aA3ZX=GI@OhItt~W>k;EZm14vmM2L6uS)@V)MXujB-Ew_4# zg>plaRx`Zh0-1QaK40Bd)oi!;2o&HPiD$s$do!P$ZTtIw5APGpmZNr78Awwgp8%K~ z_S)v=a(z>_wvJeu<4F_9Y5n5fz@Prgj~yqfMoM<1 zu1lac7Ox~qdD=ciPFGPR8ya=|L5N~9$!+gMd>P&UDs(r11=R*#3@Jhp zkuj$&o+&5nZ?%t|gy;b_vmvHhU*~N40`v;mX-7tL2=s_$CyI;tAzh5-FC<0d8N3>hYj^iQ8qHD!9CZ;~uijm{MIKusczBSph11CL(bSext_m6Kgpx2iy}Ug=WkQOq!CPK9m?e;2Yfs zmM}kkQ|aONaNr}&*^~Cu$88^f6g_v+d-`PU_1m2{9~5qU7P)rCarvz4?x(4rzSQ45 z4<`=62Ok(Od}=)ZsW`9rLe8qun5ncO4qwH_6VaLCIpX9h;(TB~M5vGdpsRu)Rt%ok zE3EE78hmE{%@<1lLZW(7b&MucenF{J#tBN3t(JS^dG$ z+{tsU^Pi~RKahO;Wc>aYxy!f8AD+p4cszLg1b6)uTsUCAagyJ&Mq~;l*d5l9Ex{F& zJ);@|qq=?lW1=HQ)(*`NPo6{-Kp8ph9*I$n)Frf)&V;?bJ2-V9GjDvTZ)C&WYqg($ zA}j;G)Rz1YoxNGRuq0>10p>#ztxV&U%q5g=()|V zdB`s44by}xMs$bXdo6V4RP@qy<%tXM`E|{AU;DoMA@KG0zIzWN556({;%mj%UqrtB zUh?3S=2xG|u6=@fA-H%WKC7aM#YvSA@*!IbuCY^NFFk4ODiOyqS}??PL(B4)4@*zH zFC0J5e{_t$aXobJ%j8eLHGKEA`{o_r_rI`QzLEL$FLYm>@BGC#_WPg0=~rE!pH^;P zhK>Vo^vQ$C{Hmq-#d9oE`q`mTo|QwrD^^68&C0D`Qrf*12HJ39P^3xnC^%YRGO6^g zmEOgJv8fXrW5;NAt#13{j}2ekl+N$ZJhoWYR11O|9ebQoW9kyC7In;m@-0l?A7!? zf0_B4oABsz<5O#x8rpZH#Q=0?5P#Wh(lL{=J9cF@Z*|R@9~c}856-fVootvoJ+^go zaN`~*^vQQU6S#5H@##h5p#z4aAL{R)bAELrcJDLenKQZv_Y4p2I=_2h|LGg`_g~uo z=J$#__ng=6MK9cfj!6t}l1Kc=!Ke~=w0;N$e1X!Q?Wt$BLBPej?P53@eo9V@hW;yZ zf{uWLSThbFX>naWmHOwunm5tW#&{VtZd%&V6XVd*Jbol_T8Ebn1i+?%&N|P=6_!V- z))Tfw%7*r8@E(>UjDQ~~z0+sZtyyN9+;3YrksDtr9zW6b&F^#XzUo`i?_N0%?0T9l z4g|Y})9ZPIQEIZ1L}^fea_Vxae$NgAdQ9TQpl_VYrjEQCzHm%=_O$Z&N3jcUDc|}C zPM(FcC*afx$L+5TfBOg555LkLIHNvx22P)ZTSt^%p4Pwl6qWN6w5a+s3uo6ZU7%e& z!Mk{JcHM%=;#v8nbHfW~nHEnsES?Sq1$j~mmDFeiw;FP$(30gI@jK76j|87wqnkDvbVgc@5&;v(D^W`pp4e-jH)PztL^iuKHlw$$zrSY0nC$q; zg=I@&R4XhTFy21yy>?o6_?Y73Q^KJW=DYW7pMR;nbj|tM{ot41dcOKbednJ3>pQxy zFKB;r-G1|A@X}%B-nHPe@)DiY=}&D~VOu&oJbkoda<6~v$oTYWhDAfJp&6mAtAMEv zh4wrG!5o;a;1IwlNH?yM?AZ_gYM3-W_0(={d#A)1(&fuSDaGnkG@W?a43JLC2Sx`T z-2zLdxi>5Ys}>?N0#4x7x?O-^~70t@ip`NA8X(K5Z-x9 ze)5F>+y(QU;PSX(;sOSY+2tZ?-;AY(+z)k3%g z=BRXN!1(&J!Sk2l$>;OG`XP1ky7Ha(;lTUOkB>%gUst~Ln(N?)@cuJ(r%$H8y$P>w zgSA7Dj7g&@d!ni^e~EL|a?z!iLC6A?gftXmYjco`80Jol?^_2ZDtAM}RFE&1STHBE zV>MSvfCQOckd*W4J&&8V?g9g+8fwaJT_Z1b0&76+P9q1%FIl2mw2%%jQ);238EW%D z3!fU$dALP$ne9t;wK;bp8OWB*sXUuW8=hRPoj6K_;!-+?ZuWTg`JJ$L8H^|?cF(r1 zpfE5pF&ZTl5)g^yUwoOLGzhh|P~SkShE$VU2USJrZga0#XP>tg8oMQ1w!+Rm#kW5& zKJ&Qms|(H>C#(nFvmZTdyLnN4@B`Ba?`uE$2wr{FasNU1v(I7EPUsmg|ata~Wjg@^oAg^omf87*~{TT6ys_lw}Uu8wao1y==1V`6qp! z-D4*cl;9KYOsR$7hKbw>RGBF7N{7qqOLEa940Oex+9h2yNBZw$R}* z*fN@P)R+b{rHTaonx(TqJG0v{L98+)C%aC&GjjB7f}lx4G-O#iN7dCwi-tO{EE2QX zecE9p9E$LxMrJqd(6o()QiF5;e5lG(A{~CG7c{NSB-id7j*i>0o}E4oDh=T9fj8X#ps zze{G0k;AN(0=|Wn8Xw;&?dl<4rlkVtR!ts5)sTDv@OVM(ly6)O^9Im7DOkM>oqxoN zdmrHkCyUO40^%_~mqn6**+SfAH)nQjkXYUL9et1ZpOJO$+M#GDu@ssvCpS@H21Nw4 z2}Gn!V<5C?CEqgi3}%~RPWs;u^MR6BN|~k%8+-X7892{BoOrkpWgyObSKdd zQqnpiK;+PK612wnFe{VycYsU}Xuc?Gl!KDL>d}n^r||)=SD8wJ$=deRGhj7?lGD~z zG2$^>l;u&(;;ixMSaA0ya{n?3sOzMdAYa)3;WKnrO*qaE&Vb$~Ix1dop}BDSg_aK8 z`V9~cfSz`Dm3V*L1vN2-2G56Q&&#ac4hm!F<^81BR2q9OEzwb0pukWBE%Fna_Bd(_ zN*m?4R;`>9T|5ZB2)E}YoPE};(zoXFRqdn!I2K)CwDO>k8FK}-$_X-~a`9|_+B8U4 zIbNYdhtX%%h^<1@#TKG=I-yG<&(^tCtRi59?4*=~T}mb@+zZ|aHfNqXF4RvuXQp&q z@VaPMm=@;c6e3w)SG8!db^2t+;F&G zUx3+lB28(Q2k|lHFbPxB;oilLm+;`78 zZ(c-}=6kEF<*L2c%9R=TM`mPX2rW1r0*2PGqRqVbWx2eQi&c@vd1)%%KX*Lz>#db& zIhK-!IV&ZtdA!inPE%}_*@H}Ntvr>4kWB)hg*VcKty!0XS~9FnA7vSaVLT4Qth@Y? zLDjDZu)PtpQLe(Q2mpYD8DXwPYTqg)O?e$$k9r<0Lc&jHhYJ`klJ^R(T5g;$8YSC7IRN-yJ+^NTjGu_? zBtpkxN-8Vk?--`qyr66C`?gt%v=5vvRR_DLdFK`gyVXn%Um8Gn$QgM9+64!)M{KK( ztY0pTo<#G4c8zAU&0^C_yAg3Xe4$7&Pm8LJ38a^sDm3xnHfLX016f)w^E|h1i2A_de3w2th3^?nb-wHI9$=PxR8F}V#v~1c;)0rx7EW}M{bl4SY zVttN!Gy6A?S#+B8T$O|@^Q9x}7J;rfyY~aLg(@cKs4ggYXkSEQw$!?NH`r7ZW}kv# zt4Px&y(qys+0fpdinS%c7547mO4^9DFJ(kGRf?w6a{zxftXC!_n~s`b?^f5iaXz?A zU<>6Az6+YeUtI|v$r;P)lrYIZEoEo5S!q*~nCjKPv}-*=Q7Mp(zeYvzut{W)s%!Rc zRh(1PYOx!VW8f(N+=V!NDtXLt!HSeC-YA7ygaU07^bYLNFq00*bk$diw3;Zezm^C)FXf_xvazi1$ zVm|dnS6z)rRW2%EkWAT^U{U<9t;4tmHNaNYoim`W&&>NnaMoyhAH-SsM&!E)?KQcz3!oSUpIs}ak;arZz%x{r%7eGUvv`}M zVxTSF$e}4mWM<^cb}olhBvL`P0AoSgs|sdk?}1?RKsW%V#ghyzI6UkprS9OHU9>2n zm`omXDFhSy_$Hwj#rG(h(-!#e?O#2Yuc(?n2Q;LtnrQEx8#p*fCy`{cGYP#P@6s)1BYH)!6zeUsrx^S8LDC>(r|M>iEWB29*x?lu)8qQn z#~@`!PW1KdXP~m>Xe`LP80ZLEaJNwGZTnywDZgd{R${RMA)lLo<36+E_~XUgQ;DqI6(4 ztv+^f2H&ViVp-DV4UFEf$~K}Oon&pchF{%5whsxO@aPUBqA21m4-P-gD^p#TmX>aN zmP0jV*PzkQ?${090FUCOfU_HId!x95omfy>pj7J@sn~&n{X^Nu zP{^T!S=FklFit_<8E4MxYJgW|o5zBH%9_aoi%N`bHI>V0uavuc6T&DHXO@VnQm}h_ zp95%NX;hB}o zAy)+!f2@SVq0dstDeZ;OvR)F88=TdT#iaVo~CraV>=_KbbxaTi2cMg>jRcsB5tXs#n z72r`lmE}kSY{E&C!*&R=eDGolOiO1P8qpSrR4teAC_RVJ!C2Lk(6LVem&D3kG9Iwcfs zjcDbFH|9*GGH;^Z58XSW0-Y6C;04M>L2%V!%p#N^nTw4YZSU-5swB%Q(;^LiM(z0P z=|TDZjfBvUluokU*2=tS$M}lvFTy^oTjm@#$&7e|BM|_di|*JCeOMMM3V1HTC0OK3 zHxtndYZmD!h4=@7Hnb1+*h?EAV6o=Q@o(C8%S3}LCuIUg-D_6E$bkfQP?b@d?XK*B zEl6~MO7G)P4SEG>zMD$%ql0wh4sH_SOocdR@4b=WyZa#+Moq*9T#a;K2C4|l+>*g) z55KVsf`0VasdPDqma#k$K=C573qm0xhVe#vPnwd+Da$Oq>i``rV&TjM1TVZ2M_aHL z6;VUCgNjm9iMAMf)-_zsoha_x1!y{neE0!rW3scI!rJ;Ws7X;uh{fc1ZVl2Ed+5(4 z40b9jM#D!ku&rEDI4o3ETlzOLR|lfQU$J`!+t6*scbU5pV9^K+k_>@j2^4B2x7XHL z%}^t7;duO2iV?axC849H_Bl&+HQGMMu+i?xlt$Tg=^9m5x2GfECflTUt1GNriNClo zz~)suZ(}j44EdCI{7~>9ojE-ijqF>&BoSOOugPLX86_@C7`c%|P^MWvDfeh}6e#yZ z?5OgyYOW@C*jggbhc^i_29OE{ZlN7(pc~;Pe@zvK(2e{H4@PvNU@8{%G5xf8)9se9 zE5NcI?P`AGR;Z{4qch3QiNGt_%racq_T9*=xz@gY~07EnrMHw0BD%Tn8?@Ihz%NUTlEnU{K3SGA7tVu0^x8-o52a zIfuRqc_ypv+gG7%xtmIO7s?P4k(E!f5RNXS0Ut0{QoTb1u}h%J#nc)Y9S&RA1rvKa zrz6J3m-DjIXv&k916dT27EXWHa(!S>@7VN_Vs)N!FeLOg4hsdqXcqBxoPYN^uAyj5 zE+D)SS;Yg+Qdbg9Z1qayqN7JU`}LE`SQ$@N z<+3YB*x0D~~}AkmG) zBch+AiW4L&?T?h3H?OfbV6%v}@D=Fkjj3nXBUEwZ%J6UU!YEZZ2x?!}z!h1(5=IT8 zxdxIp86574otv1$VMIi{UmYeXqLWqefA+$@jgTW-fIX##?4iWVJ0Ogpk>ey;Z%R|+ z_%z81*@DVM#o-vx(bpsjjK}s=inib2J6fAc zE7#*M-iAu-l0;Sxp^Zih-nC`mq_9K{$K3m9ZW1qh%%?c%t%%}K&#)=9#~f#*Nn)qd zlSUeIM{)Msp&p$i%Vazst#z`b~mu>u)}AXcZK#mKR*K(^bQ!5}|v zh;zocF3DD6J})XfUb<>*Y2>VSS_I?-wP&Mq<}j#EQ4S4)-GIYUVzgiy6WFy*=-DKN z@Dp|x38xvm&LmT0ceb3LGh7`-wt&=^<}J~%(d(_5l4PO`H?jpIM>+=%&}yn^@{WuW za0WUi*H%VRN&1ELni-{;_6pg*NlqBo3& zBXWSvfn$G&C}QQ><)-Rd`TzwF-^6R_{cFHyb>vHU2MHi@2Kk6z(hRRHyJ7`DZZHyr zag5ek>vwOX1vY98QJ@9tV?Tldl_j9x)(Ac$c| zRab1-j_;!Ws$j0E)MA(vDAX!<#H{${Xp)?nox)NR^#X$3V>i^N2!B0Eg6b(FA)A_~7m@iFOdRsdVvChi zj3Bo*6jo4-U^EA8$qdy7!J$)fBL`kzWZxP(JrO->eJy99%rrXU-ASO?RV*_PA84LF znfHhJED;~O9ACEu%4)$D2;{p8kb$?XjMl0Vj3D}+IZYlpOe`c21hPvF?OX!{MmVms z0?Z@?ND%5V8sF-2$INjW3L8@b4!vS+OMJ&Vu)5@UPE8;ZQPA5^G9V`zG8x6@aITuB z_NX)F(mX9vh-ibnyH-QYr8c*sFq0v`P+G>GAjwo)ya48qJNNL-NF?l> z(csGDU|c_S)BvU`yb$)NY4TC{mtIrigUAnzK}~XxhTyWv&{jqIgJf}R%#uyBs>ctHvi^oCzr?@Sc&&nRkx29kbB>RSC>a@ue+v9iGz5q4P zKMQ?uL_`a_Cm?2{7{-$_YbLN5(py#vwQOR7E)FC^dhcooJI%QQQioQ^6`B@cx`bxS zQe3@E7~Na^*Bt-=|MW>jK~#@s5GZ$Df?%L_|8`eh+uj~QcqT;vMXzy||Q7C!%E%Lx(i$A*oqGTzo(I}*RP@nK`Ur)BiWY%i( zA}g1Um%XkMtQH7GGrRZGBxu~qRZ;O6EnihwkDgP-7vdY_mTK}Gcp+iY<~u@Fh+;7I zMl^=~#TYH%_0*6 zR9HpDJ{wX#Awvt7T|!18hpiygpamjxlvU-Ho6Ga-7vnFyZn9RF=sp~nohIHO+f1aP|PctJ$9nw(g-fvas0qA44hOS-~^g68wF zts#PO?hM0>$&iVYbl}Ac(XGp&ERDY@t#zE0v=v`YwgBy9JZhgdK^sIhi14yl-B4YZ z*}V>YUI>NEmF1`bY!UzoB^crfMKjyhOi_B4>!mnvW!WMwXLS1-sH}6Alg1Ip5z*K& z7^P$Ce?zaXRVVe5){Fn**Qj!W54G?x$}f%aEl0H*~{A@L@|YAw;6L$=+jhva1ZLdyQ11!Q6RZzJvC*rW^`h84W;K_Fl5Ws zs+5u>5X0pP=?%-#YLSAWcdja69n2gHDUlo5*}x~7t$D0mKo#qH`+V+?OOuRHs~Zv0$ly6Y*6 zkvOyTEtXFzT2i?{EaNXL1oFjXq}<)s8?w? zvjs>)^(jxZM3~J5fU&8vw2XXsG#1TOmVCvOvzYW&WX-X#KcAtt1YQbD-?-KWgq zpp8qiv+PEjq#b&AQ3MXim^-y z;^F-Ec!Sl*Y-SgZ1p8p5`=HPhAJB|hO)LvB|p&J_#w`*%Q7Z>x_a zoQ;}Aa`Y07T!Lx#Btu6VnGKd@isFvmkIMexy#}W?eT;SdNU-=Nm!Eb-NeJ_3pBYk0 z^LafkRJ@>TK(F+iap-$c#L!cqMG-6}DdsoCLSVL399SWA6xe9lN*3b;UHC#Z>{v8g z9!&d}5!Mr{NyYFTs%Q;z*d_dk=Zh02h5E6xSUon`Wi|)BlF!3M0^p!Yui}eq_z0U$ zCrBl_WSf=pcG;8!$j&tjphqh>y?n$KLa=SMR3V0^+E@rtP@`2k`XP}}!ZF?-L9$kD zS*5cbGHAC+U1X{F+-8W{!0(n+3t~$Hk4OyBD#|7gR%>AMTwrLHiIoH%NB+PjTD>E1 zrAVq#v!G9rPyi`vv0JP6Y?Qq&(PR_QvjqbfrNo|F1!nvfJHz(#I>J_WLNf=giXYS`bXn7A9S>~*FUXwFmOi)vZ=+0Nq(T$}1-gw%J5A{F4vQre5PdE^ z;MWaaE?K7T*?`|-3v?5{9SMoA_YxWk4d@r9Wh^2#ecpAULqc$glH6KZ+f_Ia8EG{e z{Vv&SaU_De-zA|UF$ze*P+;%#$P3zgZjv|`JIRV(g{JYTiK2haAp1z1)P`_z9ToK> z4o3_YhsA|%90SP+^(V4)67-~SOqDEQb1>u2=ysPL4mvDkn`~qIs?#x`7Okd;#~y%y zyRc?~y|V?}EQgNuhZ8xY*DB)>+&24?HNoLiShY$<9)OBmiP4+9^HV#esvw00B8EQ(*z~)bYk%-3ca1$UcD`9)?IAqYkv^l6}r3``|&O z!DuKLy2tlY=TPwlR92#;yL(%f)z9E1la;m#Gna^MP>={o>~1TY4nbT*)3GFE^h2ua z^e$hi_9k55VwwH;!|XKM?0^@(1t33Uc(^y4GBI||AYi}H?nqZB#AR|G^>T<(rIcB71nO8S-#jz3nC~u zI6CYGSO4sGh}*$w!oUol;#3nxNF0w^AwtLz*ghA^C^yN(D2Tqauzv%ObmvV7E7>U? zUHjlQqNmGNb6^u^M@xa!LwKoyQIEW_0YVm5SpdBu*=l40$2&Ou#p{mmSq@$_uo#%q zVA1^5Z{9??Vw}Po2&TxqfkT3`xhTkHe0w(20_Rgqj*cdrIk*-K7{KsmFo=?i5eK!x z41yqK66IT#QkE3b2l<5@!5@TelFas;F{k2(d&)Mq;U|bxsC1}AXED994ZkOTqgkUc6NO3>Pz*tJ z^&M=PJEJS6L|G#RM&z$z<7izJM|*w!99B>mO(HFnu-pFs+Y{&;F&dBW!59R^S0U;m zC0M^{%0oey$GV62TQS-pBcZkAs@5|5(&aB1pXl8>IrjAIaWJHXU#ZBN^yw1`-nA)Kqy6RRdD@Y%_zq&b?_j1~52WGp%e;uuN1!HXwo}TtM&5_8yq%WG5H1B}w80S{V(^ z3f>2q^)s!5I*1=ZCv{tKf+a$`xOG7eM#@nx(Ndxuh$tiUc+HXp>iA*wD~bkcpxMMW zDLQBgqpG~enw3DuwJB4~VftI(P!M~wubgey0=(=2gA zvG(a%+OdKnRWc;x%8>~9EgO`ED(V{I0;CV}3xAN=vT7bNB(jm#*k}=+Q4y?i@xWTC z46Ped5+w|cl$&ztoE9+3_7S}^tEh$=2AG7|(87u`$0JYCG-Hs1FBHIJDn7fht1(F> zsd088IJ6oV#Nm4_4n_@htZ0LDA(-kofPNSaF}@Df5>1X31&$^Yu@w(3H=x-eja5;h z70%)eJVOUPVVjBt4FWF;HY2z>TD0*)ror>_i(p3}B7hNzgKsgBq^M|@4aHe=;{#Fm z5KkE~ctJ;x5+mvqxzG zuSEF~K~Mdftzx1AcpFXhqlyWJ_=r*Y-oq)Q1cj3h8MD$X4SHnJ2ClsE+1-3SDlcc{ zEl49TdjHO zgxrP|tjH%yi=Av6hePbbG@MN53m{h^-lsD+dlE68EE2+VWR(%@4%&tl&cdjn=~rJ4 z&0b>eJ_MS2NyEm;Lq_qP1Etv4S=`D4xs!B&irP5FXEmfvcrK7dAa`yD%xH0rng`=(dEJZxsy;sZ9MdrT! zAzRW$jDWgo%ARiP20fY^wk)wu?!_z&>H*C?;&1lh{nE?kJNxxj$}7#awOk>ul$Yz} zmDY~7(zcc2*nV_Bhb|If60t{_(3#t|%!va`m+ZAY_|75F+6`Je;;jSfmn_r!9@jqEyvVs8;5PBV$3@xwh0@aRs zkL0rx4cX?1fD_QflvMyWTiF7+GRWm2T?yqa@mYmfB)$us z9KHZqC!lNNc)O?RmF@T*k|g{N0h)?hngBfusa6P=89({VGjEA~_-N?WPpj_@y$3_j z0b>6#nT2cPd!K=d29ka=St3QZv#63fNKgp_e$-y{2iu>Xb1zfsDSn53$P2D0WGWz0 zf=o4JYamkzxmt+Sietv*cI^Xq6!KM6%hSM2zhI%0u@T%^thEQ%xMvLI+G`PWK|X8h z+XL!S{+ML;YFA^69x~3{)3_a5ZZdvqaO?$YFM+u z{>&aIt064TqjHfg&4^aD#au1q9yZG@%y@%U2?ABh~gU}K@G2|lch()=m4mZBB)jY5} z)a8X_%9<|NYHP*vQmk8#s)9X6$>mQs+y!Tvm*&?}n zGYT#mUaDiDqwgZHh$yy;xLx^A;r6CT-#%!}%(9=2N{8piQNDQlLGzjLqgo7>Hgt`=U{kRLS? zQfOrmbRr@`1WB?EW~*UXpZ0AlS{BS|pF68|#r)WY1x<5j*DRV{Tr?AkETEu7XQmg} z7u}cZKfGbt+Nw?4VcCL#4{tS{JvH|3ZRe6X`8gB8-{n40?-T{CHTB8unJ}(y`Lgl_ zb8FWxtKPW0cGJqbl}pMO%=FEg2p%&97igGyq$)WpsLn=5k7`}KGO=J;Z0F9_uWzTn z|El%E@uu0!BjZOyh;m49)CdZxH%O9P0wxYF+qAsrnpN!!7F5oiQJO!yZsGizRm;Lt zCV~aQ7eaZ#$V@d}d@NEsV|M3;UEZb3EuX#C?QGQ9OFzjZTYv*_h`lULw1j0K6$I*}Ho$L4gmZ6T~g>wgHOWrd5BqU*MM-+V3b z!5hs-4h7zNxAM!Ap7%eF963?=@PzsNnTqco`9C?7I&rG->F1F*Uxg{-AQ8u&`l;i< z_pXo59M!OVZo`^I9cxz9u3F+-IlX<>w3fMZ)utv&Rft!))~vC=zgs)@n(_3B$h|wY zfAyQPpB|Szdf4#fhl;Ns)c*KO*)PB5|L`k!=1t+n9WY`nxD#OYbfwBV2KB0*Gq-Nz z*0LoF>eno4+_125&D>t=R<_Jt4uNuXmqMRG!3`^XAG`@izkts_NngC0`tIA*lW&`z zeBXHgk>}@cqCebkdGdYjci(X5K86F^s3L=IqX`om>L!He4lkQHq5MYQ-372Sr z>EvJx&(O^17-sW)B0}I9O@)kD&ZJKqojm`MeBx#2wa>Xz$KdV-xOzgpa^8LIs`2tg zxOg0HoQ7Lhs&GzHR4bScbFN?lV60Nju#~=U#7>zcH*j!W}uIoaH zMHHk_L69JdCU(s2r_;o%O2)Td$)39aXTEU#d>w{1k@U)XjdDWFi+C|Vx$RM7;~RMRWUl8}!=A_&O{+f&3=cT6gnFl?cEVXPR53p{b? z)6aY25O?i3{CYKb`AdjoXr&W@w5EtW?OvdAmW5{kaCbP_Cx2KM;DrbScDK9 z>XNFVeUwyDKl{LRx#OP*hYwrN zT-J`C6TbYyc;=|_^cU)vpF1zyj2=1z<@IDTMG+Z5b988#Ep|n;5=_m7t@Ij193s@! zrw@G)JAcx1`80g=j`Z>SjxP>rUwk6pxd4}s`Yv9Bn`hwsaq-4gIC%uForTLM!xzt_ zpMM@|n}}z$)PvxH&5P0BY+5S7a&RaYK!LW_zMme6fBcE@+~=OJj_B_`qa6O+ee8^O z^rUj;y!y#8ID3jadqzKbhC6b^`oU-Ji^qH?KMuV80u{@eG3^A6$B*xK>O%hfm*VM{g>&zVUmS;b--Qd<=hO1FYwE?z(wPgk+h2>RH8C#ka^X=e_> zl{0YexPJBwfBgb~=ZbLdG=gwCqf-;K530X-PdjlEb;)`Eve2Uqj6P#iy}y{xMY5TS zYGXwnR1A!0gOSnhw1oohQqEkFchy%p!l{_QM8uC|y5G4VU-}TfKFojqO3kg?$|oP# zP8|t-c_jG$C;kr)d9IxIe0@E7<67+G>FBjD${t?KojH;`_POKhv#@XiB$M`BnZKdv z$a|xiq&1SHT^|^ZVl>4vUhy&qyv7*iGFf>!fKC}rli5yT{KgG6zx?RCa8>&034D7V z@;E32GDeYdSkTXFQZgQlvP?3AL}^hN?qW!$vYKlq2Q4n3)0QMVRh6ViIb_j4P0x5s2nZjoqNK8ampb zu^wCuE>Vt>ODn5g74SunV9RIqiE`uL{w<2|_hh3V2^3>kz9mUm2v zNwf4Ek{LL02>x&b zj(yIbIT?QOEw<{)_HPkIcyn1vbUILjG%~~VAx+gG-Z6-faf{KEfN_Hqa2(v{cyLxe z`+4T}4I?&n_OR>bg}`T@r$72Q`^BN~Cx>&Vt^_|hUUu@F=bd*d&Y!Qkc9VPilkCMi zRquXE+tqk-8YZNwQ9%>NI}pC=VATY$qJac=u=2>K$w$}V!pE*3ZbrYoRCE7o?8Db{ zUw&pje#m~|ME%K2mTMR7moLE{PFyNpy%W81t8n(FOy+uhdsIp{Wuv{zdOVq9{{QDWe5Kah4r~vA0=lsEwWY z)cxsurc)oeZhcX8?@G60*IKWAoxOD3c=(9-)OqJ;Cu2vi*xx_u{NjB6>+dX&4r^CF z6yJWeaQduc-!8Pn)>y$I8}c1g8B7B-0913<%0X$o6^96a=M0( z_$0U0soM=6uT-Al+%9x{C^7bU+?Cuw&lQq$obFV#5??n4`Ig^ z+uDs#T?-BM(AWaaolwyTh+{_&hK_E~t3QnB2IKqJA31D!>s9#rqWwzsX6T?zaP%mIZjZm*_6|GXHoYoy2Zh{Pj z9F&&`qzEQOc7}>&NHOr51g9;axt*Qe5lSet7DjJg6&~DzJ2&9#v%VW=AYM|dD(rE} zp`b7U4FV4y-WCj1aD6(dIHioh&(2EpJLq@K0X9#nTZbhTW43_UWTRwZg*GX#S!@3J z5&ZFMIP@7@y5fBPZRYzaBLm0m@cRR8ZL;5sk5GX*T5d&C$<`13R_LtuHnpM=mg;&W zzPkd4_vP+fmgX)&AVcR?uAT$K20=q3G&T^U>sz6!2^!m=r3I$XjJ^D7{Mbpy$+LC$ z9%+3?5@qQJpgaeit&l9Io_i5hg|M@(x$~R8y8}nw^?h?0wyc2B17X}ys7ygaEwt9c z%!$z67Y6i&e*K`e2Mihnvu4{@EH68GT>t#@>Wi0P@L;Y~8K14Th#}R2r;zKj=4t3d42lgr*K?Zh>AyVaGF} zyLZLQhaEqjg*Yugp|B|m0zwo=5rSc^zJ>2V$tTWx_H7B?J?%ODW!a6(uxugC`!w}~ z_I@ySA`BZr^(MOYf!5wo(*^^_z>K-DVj};-w(_rTT8y&=eE zh=z0)JBDu-L<;_sU%GE!FkC+_AA1)zEazv=BeYf42t9k#ltf2A=`GMlacQ{3H9l+xB1n z#2r2jm#^9$+%c?KN^Z?c{)O#r1F4vBly|MAbM~^F;9=`b=wmZs$VQ>59>Tu-iBHte zUN4v4RWvyW||&+n}ulYOA2L z8(_0dRfdTppdt+cFSy-?${f*+_6W1d$*MR^8dkV=5o1fk%{%RGYG2 ze>MK{1KhqQe{#z9%Wn){9DzbTm2A`yIm+HrPAQeiAs;>`BEasNHbfF3fx6ei*xtqh ztqKQMuXFxz62ARJn>iiTjkBjIF{U;FL6*mjP|GYqJ`ELRph#SG39`t$D2y6pJAce@ z^Hym8%aoUlVM{E|O&SK1heJm%z8sB63A*)zK-BT^d$yyWm2TbOdJO{EgObybv8hE1*7}J*a1*1K&k>7G01YrmS@&h{_8KUCwG$H{{&n28GrgY z^H;y~z4vx;^Lmsyk_&{{qQD1i_7dBV0GzL}QE<{!ic(c<+q&K{pgUCMrCx20^GD0> zT}2*8E093eXfAJbAy%hQk#(@?JL7WSiS(p!-67Zt8ujIxzDV3Z+kXA!>oWN zQJSpt<{EgV&rL$0xLpO@bk1B)% zW5o+a@Q0zfd+w`q{vWQl|LG^=@x%NxZ$!WQL)mZsY}&E2eB}b#y+eyqf<2WsB%?H_ zsK>KwePGd?`c3PKb7wcKSX{eiX=2s9`t55ylSWB>J0M8gC5);?ApzoHaFg@e7wWN3 zJr}R)^VTWr4>bPEf5eX*qv=AU9c1h+4+d2w>YFWP$LIF!YgI1UCPARSY^E-v_p13|g3ZbxGDD+;wBDrI= zY1_*5ij|$KHx^bd&aGMtt5;Y2?t9DCv#Ir4Y3v-@7bTscGpVTYfD@`RIGo;FHnc5V zNH4P(y>%7z6$c8&|6kqAAo4dGLUnD3g2|}k6|tr^E>7&g2GNtaa`*1T#nZ5PCq%26 zV^sXvG8occy?SZu`c-A?msGA?l-aNt|7}{aq6k$5A9RCV|8%toQl2c^J^B@ ztzB8UYH7vB6~(p7^KApDmVz@GJ$W9!z7l%r4FnqM$i9tRR@5w8>RPifws~#E!iC*e zuk@{542NEUU%uk6pO$+MBTOec_pC6*TP8{9k>e17Nj@t z&<6B@LYXpdlH<<3+=-L4=0wL6NdB@i+2bRvLqj0zTr%Fiah+$wJjaLugyRqhLVj*5sLeBt|WtjuJD>rX66o~7bOz)N@qoJO5ys!h#cxA_q;Mdm@ z*ROcCAB1d+_RM>>Km3dD_rFye$(3Q4PC{X(Nkq=$GF33?Awz;;n50)#RczZ3*svU` zDr8DNmvxnP+u|c0%3Q=>O@YrYy8q^np`UNYUfF>Gk=|!i!>#Ybm%oA@ovvBqtClRH z*3hP;!=@0W{aWTsb3L~aq6saNH~T^+pC2rGX#5Z;6{3q4rFU%v7vg>1N2}0wB!cn! zQSrj3;RmcVWDfTyiPX76)%JW}(md}COw7hx0_TBf+FFr!& z9N4zTIgw@cVYf30o7+uutVWSm&T;gL;vX|Azix$V)dGojvRcx2Z<%i&wG8dgh|hTt zz9EK41l5p-X4H&ac*2C#qD53;fk#+N=)#GZg#)QahxOZshOe$bZ7H{5U1Gpsi23<$ zwU*`v2q37;a-k^n>r?T+{?+^BYpJG*nQ6+@j8w5i9R!sKJ#wLndGpH_Ev{U$oO+Vz58%ZKZhg@*`ouy3qddE2zZ-2GOz3dN<8D-t5^Ade&GM;^R5GR1tSj4 zhy+ZnUTR-7(}FGmo0~8d?tSIDaSpNx$(z*$)EWr`{%(7>!hjB`UmMLIC*pi0&L>ll zPFVVNHtgBQ`{$TqFA+fS!mk|Q&Wr45jA@JX#ZI578Kuq$9VO!`T4hC z{N(2Cn_Xo&2s$7Z^|!Wjh~vC2NMsWjJSx{t`~UK*?fMBAz_vY@thUpK)o^W7&i`lQFZxR)9w4|_dX`sm1y4E zSS!hu-5?o|%#x(Ylvt+eLuOU{ zkN+<`eB?j%8JGz(>t(gMvp0eluM3F;daBUc)-BedgW1Hm#1i#*%DkctxU$;bnRxzr2>Nh%%ut33n?6$i?Vrqd zuX{JKJ^VqVv~az0_rCqLXR%dv^oZC3)GN{o)$7hAGV~%0`UJd{bGCIftlI$9RsIdD z{N4J49`HYU;JIUiA|;rNj5`kNt~Ej@*<$A_3RX;eB>)0hfjEF)%h~F4$b&m)_KG-VPjmiSD7vF3uc}2u?2n z8QD-iWMtWrdFX**SU30F$;dF-=s*#VHj#c>CXKK>{3ZY3C(0nik#bO&Fu7vaE~qLx z2M>(&rSlQ7Ni+Zk#bm6kqh>Vo|rog<85o9h&D?Sx^f#)i$Unw?7eoz@b`b~ zzWI&R$qs15PIq+JuU$`m`$){C!K23ZZK1+7z&mP_#+VmDOX}bO7=YwP!=alsvqdLC z_qH|=5^)A=_w7aeOqrL0#W-ki?yIxLTSq;YjzT8oU9coDVl3CUZ}30=x8w3vCgTvM z8V!u~0&y0?vDD^`Tvz>nlqXs=WqEw8mM*xW#cj{8W>o4LBQl`+bZU@_d3^s=^=pm;Eb`&o#6-s@Vtg`gz1#X9X^D0Y4 zGsLPwkG}PNb&3`{X(NZg0v9j@d{V$G*Ot|-T5M|X4i;x=`yNJd!uDZ|!|`rU(0l5d z^VW6Pxg|P&Dp@LCv_vCr48d@m2oLe<>(@DocK(ei)D^oygYpLD4 z1(GQ-5ie~+%#S^`8XHO?rIFLg=Y|{KIv+l!jlER!Msl^akJ!2%%sOA&WP9Zeyj>bL z#`4=gs+Z2R&k%W<1vpkc=V{fVEC7L${d=+Uz!M{ zYxj7TpN>~{@Px`0tB{p)V~u6w3jD;hWDWnD-@|)vDH`~4mBc(ntz5F$fLJ>ev}JRo z1sr-vnUzW+DGNQj&(KbWRT@5Ae|Xz+@}lLrm!XDD$r%V$xuR)Lh|P9`7<~C%&p-b~ z`}}?ULq{dy1bM>y6@EI#(THSBWCbcmojCH zWz1+|9>*&&hq;E#7+#546RI|>p|oG@S>F+%88c9toG-nq-?`)8vJ*d~i8-{3>6GTA z`2u&oV$%A}hzuc{hKc?3(SwK;oQ!fDZlKW`Mh&n$`ECBk-{J3gIXhY!<}HL;q@yo5 zd4i7}nucnP@+?ZKxljOI%AfeedgKG|<+HGGE=(Cy*s&2cTq-m;fBG@;&wr3uS5dA> zl;|-*GMjx}H9t@q1`X`BWEt3MRE~jULR28?4xV^b`ue!?m%oD9 zD?tw`%*M(cEy|56k^A3B#XQ(W@2)kFC&4n7hd7@N9Jazs&jZzrqY9klwDI){+Jbso zwlq2`w{DS>$ZQEp<;7EnxG!JMe}4`7cMpskUxy$iv)jD%w0I;`iDP38TNDOJUMhBQBV8I!Q}LvzHYv9 z!SvRv;h|%gCh*Qs*zaY&i?IlX5BL1t?_DRWtVr*=8djVgbn5L zQg9jVDF(04NZWe}DvBnj;o8^E?;eXC&1xpj#}mC*uJF_nUxcb=bwAZmvrnC+JpK{B z_?$Ny;wh%mJS6AOHS|I_EYo}`s)qu}UX2(ib%meSNn_%Zsi3nwaG>kKeb?37G}{M| ztH_R6xeXBz{;qXv4{oNsM*FJG+|NJ5J8zg6x)zd(pFy4COn(5>Pi)Ik{d|p$Zq45QCP)tqxpV7bm+P?wAMUqdvqQ#<#*@B)U$9jv5 zK$*00(KzSKu>x-s%noxR;V+|P0cA*U>6<6zzxgYwFfT!mCG#C^-2h2%Foed82#-M9 zL2WWLqx*At^NfjwuP-_;9<%J(4Z4nTxLiwnPQ|7U@1t)5e|RXB(FT5+*2DjxmblLcbY;+O7a*JrDjgmYisR+I3f=E4hLm4CAKg1U3p8odl(~Fuq4WI-Iy+d zR9h*3dnb7Fs#sG4sX$=YYN)CeJR!Y0NtrrOlb;`KNyiUjGWLhCn9nin9)5 z9MF|U7>gCQZqm!^PW zl`l#GDpG+aNz@&V8{e3|{#xnR9RiMUb4z9zodE?Vn-EGHUwj#VO|09fJ^5KVeq4)$ z9QLmHY)CDfXB;sY%x*&{Lg{CDtDz%LzEns1D40eMNKB=wN^-B>-Y4IguiTRA=uEe^ zj+RJcUp65k7`yj~uBQ15|E68==rO$UD*kRSmZ)yJ#}-?+)X>r0kSUmKG?zppMR5Dn z(t&Zu=E(3-_>0)sYItxj7JW<|^P>7N-f61+h$y0mFS9aG1s$sBKXki?=v#{Q2P<^1NJ;rLnk<=1Il z11%D}tnNTG9(Vga;P!dGI0DjZd$=@uW;a#l#itE=@Z+~!5AK=z4g#H5=1lU;oD(cI z`4dH3mb8mk*uFHo3@w$$s)C+MdxwnAE}RG0{g+-aJ-F{%yB6C)4Md!6_XK$KvAkdl zggIHEEM}9@Fl}&R^*n#57|E3)Rn@YC{E{}LH(a}0`RgB{wiwEI_1a3UR5Df7gJup6 z9poEE`w=kqCdF3JE_tf9Z;qb+Jo5P=aEHL|)++MGTrknv2vwD>|M4%0KRkjYAzlZAX{oO%UC^0RqwF#!ZxhMlQf+bF+ikP}J;pAxN7tX>HDJ4nDV0v;?7X zfal@e(CyQ7jwo%W=-NpNn#&{mBZ`~tZk0@&)zh+l2L;BA%&sUA9OEPH+-2^@dBf3< z)b5?6g{mlctR|a) z&27)`f@s*E$aZZ>MElKa7WeH(hU?c1L;BDLVUr2`e%2}o*>cw#Z{fHKdv;rY`Gf2H zMasbY?<+B9x@*crQ#|j>6qM?;)S3pwf9MP}92{pG(Wf|b8r~+>)teuEYrcM0Z>8Et zUcXbcM5rFBpcWcoYE3E?u^ZuAFS`;*(r~=rl zr+ZGpv~hKCI8|*DYMTuYZwnVL3a>l|9krosA|FWwQvp*fL>31h5y11-oA!VCJMpu( zb)PemF9tJdnJVHV;UvxtG}?Olrj8!(9)*L0n*X=?!pF zQ9p(1ry=-Ig8_d!5rOiG!lkRAtePtKLZE`?l~U#qd#67C(EZJA^I)>9+Kh3o(Ub7M z92huSO(AcfN-`b4YcA^r$jf2G)G4lS6A-(?%Uw~uLM@i1NEAc{;@%h!zhQ>bskL=5?AL00}dHq zCKTpR)0=ArK|^JsCXm94NvW?HeB?cFI*kYSfhmTr!eODVN-e#OU)=~C`;s%d5vOH0 zt^%r=$w-G+B-ya*0QTMh7EZ+=6n!yEiKUj2Xr9c5RloqMXW)QQpL|6Am5wdN7*kQZ zYFCUH?)=NY*-xIMsVuw%tAL|MoQ0qfuZpaF?|NDyru`T)6*I?9AxnrRDB3aebI*cC z1NV&U*ZkT(@Fp|MHzQ|=^_IFO$`A=Yje1Wz%RTSIFS5pb5WE7p48k5pb_wA>4ngefk;un7x*R)PlKY zDtk>iX+&`B5_)clX8G`Y_rP8?)2NmKpUW%v@0xCXZD?atxp*nwt~%+4#%BLwvIU`S z`?OzvtGr2b6C}6D8k6YUw1}^3!11G5W&7hgAz=tm z9urhHqk!b6JR9vQ_wfhR_oB(sgzrfT~NjB7av=% zU9w=fh%XvHG(2TCeyYnTvKYxxJ~>rppj5`EV+wW*42>i6=6mNY+YjG5S!r7A18upa zsW|G6j&=k+0GpVzjOi7gGai2-YEtF~T}o#+?cS5jlV6D+_yp~TRd|iOU24>D&q%@r zq*Hhqu@Q8J5+G^-n<}B`3l1Zt)6!K)=KuaL#V3!s2#uLgG(IIEEMO4^FJLT4w!nVl|oDpmNGthKwgKr%s+YIi{p0|%2TJMm_= zJ(VtX$l&z<`7hJq<6WykI08vVGGg!8_q8Rm?{Xy`2i0zW^poIeB>B)EhFGgF!de#52_pSG_G{$T5_ZPXi-QM`<7oIe1em~VX z)X*)X7Etj86e)}O_I=0YtLD+n?#3Y`Z!Vy1txy=P1LXeyJ-r!w-l_q3aL^+qau%ESz&%Ir5@=h507fCQb2VRIhXYY5?}w2{qit_pytd7s9bj4}RMwk*`j$I) zR(}6AI?D@nlW8eOi@OY`pe=ayRqwz5v;I2SH6jjcLQ#BH5TIljr0P$W!!oz6_B=F7~+BaW?w`{@R z@jHzsOJp<$=rm-3SGqQkA#NPm(=ly0>jT;zw$%|8uy1e2!(YQ+KVqdd$kNkl1;e44 z5hJ|J7SM%X(pW?e)k&m0ej8a_a*d?cIK)CG^y^>e?mvbYxks|E_#SE!jR4{TbYko_ z8G~(feDw0@qKVL>7R`hac<0k91ZY?oPy$Qr#XX?e(d6O}%8OXp&HCt3;?zmeOz^dG z-D(kIT5OE1ae&R&x?>Mo28Bu$^Bg-W2ZLh+9V4TICzM*dn2!aCFoME~CY1se9d(gr zikOT^0|ur3*MHehohR}jLq!|n8Ty8131z6rhc>Tb$G|9F$w6v_s)GMvHUaG3=#IVM z2%%E5qG@2Pt*hFy7QbOB-1L88_5%6Iy~s;s3uI5MV&i5yl2D}WiHODURbA`=i-hQ% zIuvw7UPFfR&~1q8@z>$oXDF6rVn;0^CCPr`@EL1)m9yKM_wSC!pZXCD|_dZ5I1;X$G=nM`^QH+8z<`zJNO`vWyf%`(cdZN_8<@fI?+vYZNMwyz{= z5g1`|hL_1KT)G)~_|V$BYl}C2N`)!uw|u#_b2t9tUA0tt^u6Oy*P0x5kN(Neo@H-u zX2Ze81r%{NC)#MUBYw@hhjcd2qWTVy&Z>{T)o$OHD6xfg7sZ28xj4PK!L^ENs0a5P z;2(Y`KTEbiWKI=_mpQl@$_faJ&<)bxXw>jMMz&Ih>{>K8K7zDND6cg<_zKRP;Xi&0 z*qIl|ZR!EtM`<%NCpBoT-NTS9Y;L_~8Sp<^KHYnnXRF|-X$A=(0V z6WuKj9O``Xz;gDI{lF`%RAd){yA*@nVX}CTGTO?q*5)1_ilT_Q=x;(tKlR_cW$sO` zUY|0`F=-q_DccI9_%N1$L%qPsjbABj0>Sxh{B-w-I=#o&H_y-Sd?|niy;6;r=`OG<L zFScMRSqFDyMlzsLQOV!EZM^%?&_Ol0n1G_|q5o>#TODi37Wnrc6dye@KTq=oBv&{< z8PZ!Zge;&cI7EluMGLAF&(r`!`>Yv>A+%;GR@WOJ-I32<)ZTv^vOyFZP9h`7B#;pY zAbjwp!r%R){xU59>9RpY;EWDHW*7t8AZT= zao|;|QA7l#k6B6>8;w6`eg7jcEE=UXV~l_wk#XcKCflJ89CvP82N8@n%o}Hzh(27P zd=E3enC7k-IK%si`FMF+I&;$|;4jg4-;!_L@S-hX-{^EN09Zh$zoq=*KQd3bJJ@6V zCo*=Bb27GxvVz^EuoG`Lc67JgejG(xfJDPfM$?0#8{Jsa?wO`;(8kz_UKBKi7fyigwb&YV zlsbvMLH|bm#iu3H+j}6!X4jbfNcH^|@gx36}yV1fh#n z`T0Bd%Y+NGXu1VsdyU)_S`@OTl<-n(QMWu=grqIs;|G`A2*rt355;+#V+BksMQ9T zIz9Y%e^>s)Z>U+K47Nr~^w<@B~w@w^X zF`2Z0OC_cI-xzN{(CCy$>M_2uizRBeX8UTIFYrD0to+?$(=&t%&`i^egDjRUSQxHu z!3Hd}pOl;fl0%RUU5>-PbZMLw?-LuFOyAwrPFyg(`Z{fL$4gQKjQ2G&`AU>nS@6H| zTH!DMtUj}czNH&TcuA?L7bL2@jNS{3qk8)%(xeI50%Q{7lh|jF@%~N2*^BP|uRtgQ z7Swb63S9}ttt{NIIn5t`0+vyIGP7qBZ7>2wxwV>XUmmi3ecv*aT)lI{9BEh|u)FEx zb*~vwA9g`OQ?8(oWzuTZJ$(my%f`AAvt5j;r{`N1vZ@=TCXbad^$U3jdY;k%mA<5zf2hx`~iRhH# z22D;ZoN5`?8`aF~3c1Vx{u*i41((6#D3KYuy*^Y`8lXz2@5>Cp1UV9_+U#ptl99xvxabjaopX)^{B zHoI1QelrLTXdh&Kd@peAIMp3OAY?Eq7~;51io<0#`V{mwtiO`iUA|=<*mMjQ3@$4d za#0CvqeK4ugzej#a9~?x;bJ+Tc7w|C6*CQ-$)pbuNX?jRwwP|mfM z>Lyv1&$ayScdn1R<_@1KiMPHZv3xG2SK*JFqF@?1@`50j6xg`P-6%%f`ES{-Ys7K`usXQjuFO$VMsPS81Bc*&frPw&bxs7L6-O`2*W%t0G- zm=c0mv3r+`?L8A4>a_1}*^XTjHtsO@=}EkEI|y^(ooJO1#;Tn6&9_tk{7>>r&!8;$ z6A{s6MMI2(puisQM~KAGMqt($XI}(ID1R746AP&$eFoWo^9YWgGHl*2mMX}wB4Wip z(CI3KFTmyqox4cMr;WR|Lwz{}&EOKy*|^T0R`1?dhD?CY+U&C%#Lnh$S(Ud~GPhL8 zR67taHnoGV(|}I~Gt=ipspKQ%-v7XS=T3;_8sVjwoaJpjz+y(NM-zfPqw*cVS$nps zqlehDb%sO|`OS0)^!9eo&wr}@n}2{(Djs&l@|E61+*!&X(`tu}&vom~`Td@nvS2Zt zF62|iif}P)OnXR0$uKFQ+H9~XXU?bPZv9&S<$uP1_bY_`%nYEnv^%S^$#k)lsSo8U z9hF5N)jlOE(NaP(TEAvlWYlm7hQp~sB2$nt$TyN%*Vx3qzI0)5&2p`&Kwd^trM40J z{YP-_7z`d%w{T8q!7RuWO^KwjEz)Iqtn;Spo9AfV+xXtC(4!uj%DEB!=m5-g%D!lh zf5p5m$%cf})>%X6UgL$tq={JKn>=)=??3)OsXO1u?3ge#^fW+3gHE%zP=r`0vubXB z)jT>~x22la&pdYUI%(T)DrB2GA-8TR?e9h*VBit2;I?^|MH8ujLU}|NOr_mu?`(?Zr$Gd4vQJDIK~CayhGuS}?5E z)(qIP1){NFc`cY!l@pQ2oHyoq{9D`ON0vp?z-=}abM`_OB^*g@2;~y%X}-X=c9Hk* z|K5A(bJo~XB>{6Ny?ZS*7NL6;3~U0g1uQy*T@ZFcL#1ceO3rDL9S(WOVB4dI)~_BY z^>l6};~9Aiq>*D_Ki`(6__zDuKJm$8>vJ#S$E?@%J?6nV32)xocfy>Ga(A!O=-m8-MyK^*8_Ee(tT* zfep&Yp5#*4G>H`TsBxNjV735hDFzEEh`LfU=0?VlEy%p}uKD)e;M&dfdYjdsEX3=Y zX#tEOCS(jFB6tV%^e>tX{+y$x3fp151yk>y{GEs1?|&e>ju!%EgK8{Gfy-6icSN?n zCl`qmFd-Hq1ti(;u+?PgkP=bn+hrrKf&6_uHY}*_)gJC~H?^Mxo=7Mzp8qAp> zP8?r9dw%_r`GqBO%9qY>TE8wnduDFznAn?d#Q*6Z_2Xy27B%M+iG~sdi6YJITd^fg zvUjUP>!75wz1e;Qb2?|Gf^>uqxl*O&(a-Ar`=(JtNIJ~caD7eJ{D>OKq?S?6vVHSP z@8AEe_fuvIx<*K{Bu7`wFRfjc-mxOQZ@G8csH%mtQcGvV7tZLld6R7x+EA-G85jHY zw%mW##H6O;Bkis4)kv#TVUU_Q+V{qa**wP08Vwg)v7dfH2M}!ORbrg zSUxked}d(U=*;37$%V6m3uiYxv&%E)>Bca=u0nrwCw2NR`0~}uRyNIEY1 z1Kq9;7^eT#6XRF6VaSBC$y0hSoLWC^YR%L+6;tQbPo3Ir>IC2H*^NK_j5q6`n)&{H zL!*Y4x_5UE8fm|9(fs6R%Z`_!e-8?kIf?3|Av*$|03B+GL`Kvq8+E4_ZGla;4I2rS zB;t`5UbEb~7hJg>31!V@U6G7AmZ6nl7LlUZAg;nun7g-GreGMRTPaJwi)Enp>~8+e zA0vPI6R_EFHdPJ~c0e|5Pn27OMSD#hB2vabbQc)O^7c}ZS8SlV!5i@gV_o=yPlr5z z^&tCKPpnsOqLpUt6Jg9e~ zsi&*H%{Op>ZTMhm%ne3|gz~)5iryj*9gjl($>jYJ}F@2u`=5 ztBe8*_${0YCdrU0war>#95GVu+g*O?koBiO1i$!1Z)u}4ZPhjS4kD_NS$u=;tgGs> z40IV(LsKo+zlSwhg2tZiU%vCAEl}K|sK?RU<9#$oL%rs8gSV6*sAzNeCrqF!Z}ffy zq9`mXSE$cjxMjR?o?kp0iX~%3gT1q-p-(TVqr*31tZU48LsKKHU8_BOs2@A67n(VX z;=tBuwihF9W(zDvp)QZ(;_XgjM=#O_m)G72~+Styvb%Nk8FB1$j zdq?4Mk5L8#m^ZI5|NghW&%U7Vu~!UGqI(eCk<@?@O8;K^^by8B9iagOEd6@g+PjCV zy8~}@S66}EBve({9zU^N|5l;X2AO_Iis-j^qG2&xG)^H$=-9mz9z8TZ$7}(w+AH!( zHX?+Q5RVx;+syrXSUbA~`}Os8^s1aVJ%WKlW1G1fJGv6P+frlv?oRyJE%21op8f5; zI+VHNy?uLzyAL+^9AfU*jq7MFKYIzT-xSu<#60m=23H+b^uZ8Y6KN6ITvOm2c5y(P zsj?Og@KalW%s~PUALse^zZS1ugJJ{5w)Xbw@UWq-{==R9hsK5uEgwCU8!{|)=MFxB z;^m;4nG~AIAUlo6Ps!i^D2|&8(Ev?pax%0Tp95G+xCDI{F$_k%Ai3deJ7@z~%&h0DXzxQb#YO-h$MpxA@Z! zf!}^lT8Nj9k+3R61MGA|?3hh0RWlSI%jhb2T7!#J*D93m-~fLr9?P+y6ZlL*|Msf! z_IU(aB&xjPsIR7ZEULC?;L##Tvb&BY$z!{*kdjDKNX8@LKqx9o$%wH)3E@uG-B>&e zdJ+slwW*eto-y1TFpj@?+j--ji2V`J!kGloicydw<8t{;v}ZxLiEZulYRO8442>4r z0^HdR3<#|;!m?AbBj)o9m30A;#UXgH*O6AunQ!^sA1p^tC|w!ByoskmG~g;r>mDCR z^NVb<2GMLLA*Kp!Lcu~+v9V26;ml~D8R{V@1|Ac#l^0dF>WEWWv{Y1>zfpPki|6gP ziHRPELR}S9S~LqV1?BE|7>d@vt5bIpG9qG9J|Dy@g3Fd4x?+xsCDg*v9d z5{n>f$3W^T?6)a1#$sn3n!5`k~fC@w+v;mQ116gJ?Yt;qp0(PD2(~rM%+x_4h zTT5$~4G&aR2^fE&Y?;hp3(eTLt8?7QwEBm4pw0(9||NeVz>@>)*nLo~; za*~Mf%wbN)(H7{SSkT=ti_CLolhC^d)nEc=OI)_b9+aY^|M%Jq zZ4s$8@-`5?#$d)5Oft_ZD*n9l%01)b`^MIKQb@(&D58wgmo{>;B>B#UI(i-;kWO(y zv`PxAM`9#I8}JyaVt6}J#l=IA*3z9|@*CP3**igP8fdxtjrFVhT%nSgL@Jk!zkb#nDrZ z-`sZ}{ZcDOgC($p7E)_lc~({NF~%>vWYCK_qd!3}(u@-21S8_&d6OetC=x@F7L?tG zt`)z)KSdI$pif@D(eUFhuGike*NH|=$R;RXjV)my$ia344G4n~t(j=wv0@~1Cg`3* z6V&v{{rtzsi6f#TBl4;A&R1l?j#d~ui*Kivg;Y)8<~{NH z34Qqz5<$GENH_k7RL_f+u|S(J-gx&$qOe5&FLl%t&bVe6#6j=Y+UpTjki$$^})jp-(NG|z6H4!kv5#Nbss@P6UeKolWkH+ zZrNqNb<_3MCmNOLF@j&guDjwPlZR#mp0WT{0DFai1dXk#xzUJFbuLb7>@YvM7(08E zOQ0Rsz+^GkltfCj!{1p6V=+*C<74g7SFr!-nbCMzV{xjcOF30&y4fKSJf}9-qP112 z{~U){uS;4MOcj6swd3LiXzHP%0T9?yB5D=-U9<~mO0;15_;CghY0ez|JEAHBuLN9Q zoG?9ltd1B9aSwi{2{sBxP#{fOu0%;xBvq1w?3~|2v(~MMqtMa0oV#8&Tsh}@{S|~Q zPRe?Mczr#1Sdm+FGF~6et6N=?=HiS_a9ZTigHfitNM)Y4RQ~CH-ESYVzXWiF!03Tg z4)O)cI6@=e-br)L5?XtMsuG9NHMI)b0x)rrlqnh`qBxvdS$pWlPx6ENO7CXu66kJM zO`D<+`avP0Stwqy)KQ>?mpu~Yl5q(8*qKNM9HQ#=$pu!W2SJStY|bBXM+BP7Ni;=8 z9z4qZ?GN7D4}~UHev2~nh@`=IPRK<`SP_G?wUWS~`|^aq=A&~&26oaU5C&T)o!AbV z9RV__q8;T0eUxgU;-xrb&cu(K>HF@X@8l7vs38@V&=?UV&BcV%N<~WW4Gs;}996;P zh!+u&K*%TaD83|Yyx9@Xb?yJhc#M}=x)2qt#Y+~&TuNBDME>E4_1TxPqq;$|l`>?u zc`6d>!RHNT4TVWEP$r(wM@O3nVkkV8vIfYvh*xixJ-i2|h~)A|P~n+L#|x>d@@kw4 zUaFvpYGWRwSs4VI#okr1R*dEX-#+0leJM?zXu<~3N_3$?g9?5Zv@->6ZI1o)ZQ#l+ zP%QLJ$ zV~TR(93JY4%T5Z6NI~EORXJj(eLLRU>K@MeJ zdq@B2D|m(SxLD>n0|1|PGIpoVjJ!GrEygN8~nuQCR_=>ItmTR@7em@PfI;W&I4 z8ro6v?6iJ{PYK{)VHBmbyGv)z(5u-hk|oi>8#XoF87Zm>WG5LWtSW+mJ4q8Y?OD&|o(A>s7gKUUcyo|B|WNl2uiI z{7dNfzuNb$4fG!iu7F(E$SDFzZI>@F>h_-OoLOXADhaK%wz*TGqQpF~;0>qk?7(=u z7rij`&FF@q1H2n&CYO(it)2x3H(CGmhpGoZnddE$dp1JwY4W@`l9H)12a1?QTRbg3 zcV=YSoZ{*QiOHivvnSX#&-N~yVj4C8tfZG+3zVgN6(uHz?8c$})3c}Mm(KRhnW7$i zweb7jRz0}oT(BhC*`F;w;a~-8Hs~XG`RU!#!YnRu}MWN|36P(Q%N_WKw zg!@_7$STmksCdzUeBhw>AOFVt#=H2X!J!4vj?zkn%1w8H|=me`Ns0#l3w42VU7}Hv&cFpT5vSzk4!<*L*N?=9nY@EUl9FJ zw1;#=gVv*a^xN;$TNn93gLJAjf=Z5Gps-pj6UL@CZKO})vGngBtJkhVt_`)>Rmjjt zQ446#pv-$BTv-|nTCh*QnsJoOg4$)NEwc4Alh|)o;{J8d*-s#mx`P)UHwnopidG6_R;=8w57tb@!oE}-e ztZd<`_^hQ+S?t0!;?$|y?{5XqoC9B+uW5{p8dh34Bf4NxYUTXcvL)HoYhc(&_op9- zkIupQk3kHfX(#Z5Ca=5lF_v-1EMbmw=7Dm4K#Q6PH&q>OaVw2ktVm8z` zbYOV!pvGCV>Q^jwE}B-@yCJ${c4W~se$Ft6pps)JAhK__?cNpt2Om*IZ?-$gf>stS zlvl1=F;%C-gth45L&PmrlqfL{kpp7m@uNzA_)X=X{=v{|B%}k#Q*@|o8GsklemP7B zkeXJ{DVsUPFsUCmwAnnkBQ}4MXW$^{J=pZYTfx8o!F>G`q7;E3vcZwaLwUYs*`mgo zGaP+74PytGMh|ihAEb>L02BL#ckGQXo6kV3?tJGX_1@j&iBsrTYj4F)V|2&?lORwUNVgt1L9x`#4xg73Q@eLp;c z<$6YFcapo?M}=Emu$xSDcGh&0;3W~P|Jy$nZhvcAxU6y0GUuo^k|&eVlgv}q7BmW-t%-f> zOyh=OpSj_k>VWp-;%P8*usVJ~<1^c&fjzKYX8s^kszPuk4r;I8y)HO)3^%YF3}{mi zyc~M+F#gpA$ATr@=Pa>Ruo+N6(89g|q9nZRMlyv)1<=XYG&+_ni+=eUVlwOnin!!* z2VyjbFW^1MTY*z_bP@O)3wv5?<0nt0{^NgyKl+sPKmjEmA#BO^VNi+MtWDfCr?7gS zZD=2Hcu!?SZ`-JW;)tHfIg?@dh^qhmC;0BHf5r?%dG@*%1*2IM3|jT$Erl`&M~uaM z?Y=dQFRlkSRa2po3D_n=@|hDncklV{U58w`F<(RTJ`Q@Lu~?N~zae{&Hc(@DoP2x> zu3j*Woq@5Hsk(~fPD0OvK`n;09w>_&dw1)xb7K>$7XaaqTALGSVgiCYUiI9%5V&*% zL*LNc`I$9~BMYWj2lqCQ=qrvKAPpO(O&SF&=2ZN@|0I3?2-2Ns{Higg$0srPe0FDK z)jE097`v|BQR%V zc7$QXsMPGam8+M70ZpnFed%@i_Su?O-vUjoeC>J9;OT2R@ zG32HtUVPc})rA;>!B`R5jsQS$ zyey6%rf!%E#VRP4$(02KZix77Ep=Q|+*$CaN?CJ78B~|E^d8kWZk%hyLerKN z>X^}Dwz9I=UY4zqsp28WvGhc|^2=7l#!Z%D4F99>baXnty>B>u25K7}lSWkSS_lP? zHJ^=Tsz5Vsh#cUZEQRwY<@X_Ry2Fl28rFC;1bA}hTEQGAzIiLfQ z1c1S6Gn_uHJig{T{~7dXu6$;{-nSp6V^J=R0aP<^7%EyI5i9%4pVVI-dzVt`o{6R7 zTQj+;{Tsl80k1{}lh8zgM0q>-T_6zMw^B~#z~Bm=`CPwuu5tMeNR*ZDS_dI?s@Q}E zX2kpn$k&pnSJ&15*WbdgXU&tQAZ;zgQ3wz1*|cyLP2`a}&~`g&9D%(-f}qRw!D|r6 zLZm!(>$-OBOlILGXl#pYU61}(D-{I{Gx0UFlNA9HuS^)%_CNoe|HHTDh0DR8RZ8U$ zv;;=q`b9sWyhXps3=KUZTQ>~|D6l&@$Y_(9n&FprZxbw?-S%K}m0@^A5Bi z7Sb6NV=av0a>nKBVB|2vs@2-<%fjv3P}6GIxyi-Y1wZd?zDHZi7(8Nepmybj)p zoz*d5ld5Zazwjcs?V*+P#R2`n>NA_es${0r2_9`$*JdNA+_PJbl%bZ{GzAB#RJJ;< zUewQ@qCh(4&+c0eHF4SN(`|^M@Bx0{`gJq|Y;bZF1@PMthUnoir4k{Ly}rh6YcWh9 zdZRQ@O);_J`7J;-IZz7eFn6u*DCZ9k^s}FXD`fDcd+yo6lBP+SQDkkJ%jAgA6iivf zIcI8e#$xcs&D++17tJwYeL>e11Bwm_GqYXDc!@$s&Con;l6U-2n71(VKmXnN;%hh} z5@;l$-^-cJvO_ocL(o);Z(4vYiL!|^m|5U}q)=o#x(b7B)0)KJ+$%qS66VYTTLk3^ zOmPTi!Q+8snBTHRnLG``UgN9Jg4+w>5*U4u%W=!+CC+>)UOyFi@i}Zn7yX>m0kyIE zEo-cOhd{Dq+t{_g0DP}J7rcJKcj_ec>rcTTsX&zx(TyTJU_iNxa@uDbrw{zGgC2MA!I zm9&6n1G67lKy(!J{7={UC&$t!zJ!)G94W{~|J32OUhSkuDs~3#8JabeLv(L4r5-)A z9;VEYH}8F^4M3qwpI{v|@rSd!3|Wf*lmUwstM_>T7%Hpy$@f;=QX7sZ(fI4au6NZ9wg| zU0_l()5ax+4J0n7YH1RfgvMfRT{hP>q7x-gKK(`cfBw6v5AB*DCDc_S7M*QF%F(%G zjadJ8Am_; z@MkOx7z6z#J3sy~e)>@K^ykv8GvVWBK=TDRZsJP`h979s0ck+p!w#s3NagYc^R3+x z($K{mpX5PZ^2M0qa>#Qjn)cng?74hWLC*kOa^8GnPqvqVY%8KxUKG)y@y6Q9)(sn! z>KZgC{#ny(oxO-0T{k)K2Jxw$03=@63pUg=9<3SLO|`x~boZ+5@+mqwL($@!7D1ji zDG~4SaZMil>|TL@CDMa(eksPH5r`(E7o6UvEo)SkYDQu~`;x3>yH{d}f&K5=yrvJI z;=Xs&eEbt|pfb41*RBMQjT2BgsoWDraTd`{zw~P^Te(c}l`!lrzWEZEy*%5NKqiC2 z_y0Bo*d*}?V$H1sQ8G-TG+}c6|M}mc*WaN&o7wh8IunI$kHV|Ita2M75soMMm6%t0 z+7jNc_UK;qe?2yS_NM>(8RM(33G-ILpi$7H6DADiUO51WHO9>#;r{G%=rs@qOoS1W z;J`Dfv*$gZ9`ap10z;@wM%Ug@%aSpL#dAO~bLol{Vqn5xpCukU34Mo>M6F)re)HAhrE`wsU#5>8hpHN3@>srK zA2yqbQj6SF5%W5D5>dP?+t%9t`49Zj!+~2zVec{+J4W920_@rdBS*r(!O*`K^yx)r zsCO?IJq8+^wL`Cj&K@_O{EYkhnDhEk5S{61lc2qTR5eN#nfxC^5F~sgjG;H4G5_(V z;mlFzrPF|bx3jP7wHNtiOJUGJdPVHVsbw!7RUTgu&V8nzIRtAr!hq3{!$)A!ROmYrhD{-b z8Z{n9j)&ospwAc>FkC$Ku5|qi?VHo$@i$}dy$RqzA4P=7S?XST8Jf{Z$-d6+;lcfB zkqt*h>n#A)lP8r=A4k$}H2Q8{ivIZzYIj!clM@75fFXGzlLjzw)n>Mf8+8Ts6Lm&& zgMg?MV5G>VpI?su)t`(vZibFtDtvw=cH&a#jgE2U$_Fnug=CU95J|2ft9M2Yxo?-NGKR0N;jDSA}jRXzQNkj z&aAbXpFP{$(1eXEU3(@qya$zv(O=+VZyy9N%c5g^muqWM53V^5y$g;YaEA2erBI&2 zTT!vdT6t_ud$v5y)WDG5NOM~%?byc7Ba2P-TUJ?YG#|mHDiz)qs6V&?Q3C=;95S}A z_8JktbgvD#yZCGG5=~JnF(wz#8ESOuI#Rg4EVpDjer2Qe z8pKGH9za`+2v^DwjU~T5;XCmO9Q)98;z<11iR8(XiQ}K;t{u(&=Dz9XP2b_O-jnC* zfA@Rmu~YFQ=OX8?ST3BGKKP_?^i=N4cL|)# z!ROQGj)|YWW;y#_#pYIMvLa+TL<)f0t99@;aYK7?�P%sJSh(j*aPgh?#W|%*Z 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 literal 0 HcmV?d00001 diff --git a/lib/src/main/java/com/alphawallet/token/entity/AttestationValidation.java b/lib/src/main/java/com/alphawallet/token/entity/AttestationValidation.java index 58e5e2311d..943e69d2b1 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/AttestationValidation.java +++ b/lib/src/main/java/com/alphawallet/token/entity/AttestationValidation.java @@ -16,9 +16,10 @@ public class AttestationValidation public final String _subjectAddress; public final BigInteger _attestationId; public final boolean _isValid; + public final boolean _issuerValid; public final Map> additionalMembers; - public AttestationValidation(String issuerAddress, String subjectAddress, BigInteger attestationId, boolean isValid, String issuerKey, Map> additional) + public AttestationValidation(String issuerAddress, String subjectAddress, BigInteger attestationId, boolean isValid, boolean issuerValid, String issuerKey, Map> additional) { _issuerAddress = issuerAddress; _subjectAddress = subjectAddress; @@ -26,6 +27,7 @@ public AttestationValidation(String issuerAddress, String subjectAddress, BigInt _isValid = isValid; additionalMembers = additional; _issuerKey = issuerKey; + _issuerValid = issuerValid; } // Not a traditional builder pattern, but more appropriate for this use. @@ -35,6 +37,7 @@ public static class Builder private String subjectAddress; private BigInteger attestationId; private boolean isValid = false; //default to false; must provide this method + private boolean issuerValid = false; private String issuerKey; public Map> additionalMembers; @@ -71,7 +74,12 @@ public void additional(String name, Type value) public AttestationValidation build() { - return new AttestationValidation(issuerAddress, subjectAddress, attestationId, isValid, issuerKey, additionalMembers); + return new AttestationValidation(issuerAddress, subjectAddress, attestationId, isValid, issuerValid, issuerKey, additionalMembers); + } + + public void issuerValid(Boolean value) + { + issuerValid = value; } } } diff --git a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java index fd5a9fe20f..a09cbce451 100644 --- a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java +++ b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java @@ -1079,6 +1079,9 @@ public AttestationValidation getValidation(List values) //handle magic values plus generic switch (element.name) { + case "_issuerValid": + builder.issuerValid((Boolean)values.get(index++).getValue()); + break; case "_issuerAddress": builder.issuerAddress((String)values.get(index++).getValue()); break; From 86692639f2bef2acb890524645d80b3910141148 Mon Sep 17 00:00:00 2001 From: James Brown Date: Tue, 16 May 2023 10:29:48 +1000 Subject: [PATCH 07/11] Check validity against keychain Parse EAS attestation complete import of EAS attestation --- app/build.gradle | 2 + app/src/main/java/com/alphawallet/app/C.java | 1 + .../app/entity/EasAttestation.java | 40 +- .../app/entity/nftassets/NFTAsset.java | 13 +- .../app/entity/tokens/Attestation.java | 388 ++++++++++++++++-- .../alphawallet/app/entity/tokens/Token.java | 5 + .../app/entity/tokens/TokenCardMeta.java | 31 +- .../app/repository/TokenLocalSource.java | 2 +- .../app/repository/TokenRepository.java | 4 +- .../app/repository/TokenRepositoryType.java | 2 +- .../app/repository/TokensRealmSource.java | 33 +- .../repository/entity/RealmAttestation.java | 8 +- .../app/router/TokenDetailRouter.java | 3 +- .../app/service/AssetDefinitionService.java | 267 ++++++++++-- .../app/service/TokensService.java | 4 +- .../alphawallet/app/ui/FunctionActivity.java | 3 +- .../app/ui/NFTAssetDetailActivity.java | 9 +- .../ui/widget/adapter/NFTAssetsAdapter.java | 2 +- .../app/ui/widget/adapter/TokensAdapter.java | 10 +- .../app/ui/widget/holder/TokenHolder.java | 37 +- .../java/com/alphawallet/app/util/Utils.java | 53 ++- .../app/viewmodel/WalletViewModel.java | 70 +++- .../app/widget/FunctionButtonBar.java | 4 +- app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values-id/strings.xml | 2 + app/src/main/res/values-my/strings.xml | 2 + app/src/main/res/values-vi/strings.xml | 2 + app/src/main/res/values-zh/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 30 files changed, 840 insertions(+), 165 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 728191731f..1baf92f4de 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -337,6 +337,8 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.5.0-alpha02' androidTestImplementation 'androidx.test:core:1.4.1-alpha05' androidTestUtil 'androidx.test:orchestrator:1.4.2-alpha02' + testImplementation 'org.hamcrest:hamcrest:2.2' + androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.2', { exclude group: "com.android.support", module: "support-annotations" }) diff --git a/app/src/main/java/com/alphawallet/app/C.java b/app/src/main/java/com/alphawallet/app/C.java index 81d0448248..373cc6ec45 100644 --- a/app/src/main/java/com/alphawallet/app/C.java +++ b/app/src/main/java/com/alphawallet/app/C.java @@ -117,6 +117,7 @@ public abstract class C { public static final String EXTRA_TOKEN_ID = "TID"; public static final String EXTRA_TOKEN_BALANCE = "BALANCE"; public static final String EXTRA_TOKENID_LIST = "TOKENIDLIST"; + public static final String EXTRA_ATTESTATION_ID = "ATTNID"; public static final String EXTRA_NFTASSET_LIST = "NFTASSET_LIST"; public static final String EXTRA_NFTASSET = "NFTASSET"; public static final String ERC875RANGE = "ERC875RANGE"; diff --git a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java index 2c38204d5c..f7458be047 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java @@ -260,14 +260,7 @@ public String getEIP712Attestation() eip712.put("primaryType", "Attest"); eip712.put("domain", jsonDomain); - JSONObject jsonMessage = new JSONObject(); - jsonMessage.put("time", time); - jsonMessage.put("data", data); - jsonMessage.put("expirationTime", expirationTime); - jsonMessage.put("recipient", recipient); - jsonMessage.put("refUID", getRefUID()); - jsonMessage.put("revocable", revocable); - jsonMessage.put("schema", getSchema()); + JSONObject jsonMessage = formMessage(); eip712.put("message", jsonMessage); } @@ -279,6 +272,37 @@ public String getEIP712Attestation() return eip712.toString(); } + public String getEIP712Message() + { + String message; + try + { + JSONObject jsonMessage = formMessage(); + message = jsonMessage.toString(); + } + catch (Exception e) + { + message = ""; + Timber.e(e); + } + + return message; + } + + private JSONObject formMessage() throws Exception + { + JSONObject jsonMessage = new JSONObject(); + jsonMessage.put("time", time); + jsonMessage.put("data", data); + jsonMessage.put("expirationTime", expirationTime); + jsonMessage.put("recipient", recipient); + jsonMessage.put("refUID", getRefUID()); + jsonMessage.put("revocable", revocable); + jsonMessage.put("schema", getSchema()); + + return jsonMessage; + } + private void putElement(JSONArray jsonType, String name, String type) throws Exception { JSONObject element = new JSONObject(); diff --git a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java index e76567175f..0c53cb779c 100644 --- a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java @@ -49,6 +49,7 @@ public NFTAsset[] newArray(int size) }; private static final String LOADING_TOKEN = "*Loading*"; private static final String ID = "id"; + private static final String ATTN_ID = "attn_id"; private static final String NAME = "name"; private static final String IMAGE = "image"; private static final String IMAGE_URL = "image_url"; @@ -109,7 +110,7 @@ public NFTAsset(Attestation att) { assetMap.put(ATTESTATION_ASSET, att.getName()); attributeMap.put(NAME, "Attestation"); - attributeMap.put(ID, att.getAttestationId().toString()); + attributeMap.put(ID, att.getAttestationUID()); balance = BigDecimal.ONE; } @@ -578,6 +579,16 @@ public String getTokenIdStr() return attributeMap.getOrDefault(ID, "1"); } + public String getAttestationID() + { + return attributeMap.getOrDefault(ID, "1"); + } + + public void addAttribute(String name, String value) + { + attributeMap.put(name, value); + } + public enum Category { NFT("NFT"), FT("Fungible Token"), COLLECTION("Collection"), SEMI_FT("Semi-Fungible"), ATTESTATION("Attestation"); diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index 4d3b950d00..c49a391ca2 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -1,39 +1,54 @@ package com.alphawallet.app.entity.tokens; import static com.alphawallet.app.repository.TokensRealmSource.attestationDatabaseKey; -import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; +import android.content.Context; import android.text.TextUtils; +import com.alphawallet.app.R; import com.alphawallet.app.entity.ContractType; +import com.alphawallet.app.entity.EasAttestation; +import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.repository.entity.RealmAttestation; import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.AttestationValidationStatus; import com.alphawallet.token.entity.TokenScriptResult; import com.alphawallet.token.tools.Numeric; +import org.json.JSONArray; +import org.json.JSONObject; import org.web3j.abi.datatypes.Type; import java.math.BigDecimal; import java.math.BigInteger; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; +import timber.log.Timber; + /** * Created by JB on 23/02/2023. */ public class Attestation extends Token { private final byte[] attestation; - private BigInteger attestationId; private String attestationSubject; private String issuerKey; private boolean issuerValid; private String issuerAddress; private long validFrom; private long validUntil; - private Map> additionalMembers; + private final Map additionalMembers = new HashMap<>(); private boolean isValid; private ContractType baseTokenType = ContractType.ERC721; // default to ERC721 + private static final String VALID_FROM = "validFrom"; + private static final String VALID_TO = "validTo"; + private static final String TICKET_ID = "TicketId"; + //TODO: Supplemental data @@ -56,13 +71,41 @@ public void handleValidation(AttestationValidation attValidation) return; } - attestationId = attValidation._attestationId; attestationSubject = attValidation._subjectAddress; issuerAddress = attValidation._issuerAddress; isValid = attValidation._isValid; - additionalMembers = attValidation.additionalMembers; issuerKey = attValidation._issuerKey; issuerValid = attValidation._issuerValid || (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))); + + for (Map.Entry> t : attValidation.additionalMembers.entrySet()) + { + addToMemberData(t.getKey(), t.getValue()); + } + + MemberData ticketId = new MemberData(TICKET_ID, attValidation._attestationId.longValue()); + ticketId.setIsSchema(); + additionalMembers.put(TICKET_ID, ticketId); + } + + public void handleEASAttestation(EasAttestation attn, List names, List values, boolean isAttestationValid) + { + //add members + for (int index = 0; index < names.size(); index++) + { + String name = names.get(index); + Type type = values.get(index); + addToMemberData(name, type); + } + + issuerAddress = attn.signer; + issuerValid = isAttestationValid; + attestationSubject = attn.recipient; + validFrom = attn.time; + validUntil = attn.expirationTime; + long currentTime = System.currentTimeMillis() / 1000L; + isValid = currentTime > validFrom && (validUntil == 0 || currentTime < validUntil); + additionalMembers.put(VALID_FROM, new MemberData(VALID_FROM, attn.time)); + additionalMembers.put(VALID_TO, new MemberData(VALID_TO, attn.expirationTime)); } public AttestationValidationStatus isValid() @@ -74,7 +117,7 @@ public AttestationValidationStatus isValid() } //Check attestation is being collected by the correct wallet (TODO: if not correct wallet, and wallet is present in user's wallets offer to switch wallet) - if (!TextUtils.isEmpty(attestationSubject) && !attestationSubject.equalsIgnoreCase(getWallet())) + if (TextUtils.isEmpty(attestationSubject) || !attestationSubject.equalsIgnoreCase(getWallet())) { return AttestationValidationStatus.Incorrect_Subject; } @@ -84,17 +127,46 @@ public AttestationValidationStatus isValid() { return AttestationValidationStatus.Issuer_Not_Valid; } -// if (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))) -// { -// -// } return AttestationValidationStatus.Pass; } - public BigInteger getAttestationId() + public String getAttestationUID() + { + StringBuilder identifier = new StringBuilder(); + for (Map.Entry m : additionalMembers.entrySet()) + { + if (m.getValue().isSchemaValue()) + { + if (identifier.length() > 0) + { + identifier.append("-"); + } + + identifier.append(m.getValue().getString()); + } + } + + return identifier.toString(); + } + + public String getAttestationDescription() { - return attestationId; + StringBuilder identifier = new StringBuilder(); + for (Map.Entry m : additionalMembers.entrySet()) + { + if (m.getValue().isSchemaValue()) + { + if (identifier.length() > 0) + { + identifier.append(" "); + } + + identifier.append(m.getKey()).append(": ").append(m.getValue().getString()); + } + } + + return identifier.toString(); } public String getIssuer() @@ -104,28 +176,107 @@ public String getIssuer() public void loadAttestationData(RealmAttestation rAtt) { - String attestationIdStr = rAtt.getId(); - if (!TextUtils.isEmpty(attestationIdStr)) + populateMembersFromJSON(rAtt.getSubTitle()); + isValid = rAtt.isValid(); + patchLegacyAttestation(rAtt); + + MemberData validFromData = additionalMembers.get(VALID_FROM); + MemberData validToData = additionalMembers.get(VALID_TO); + + validFrom = validFromData != null ? validFromData.getValue().longValue() : 0; + validUntil = validToData != null ? validToData.getValue().longValue() : 0; + } + + @Override + public void addAssetElements(NFTAsset asset, Context ctx) + { + //add all the attestation members + for (Map.Entry m : additionalMembers.entrySet()) { - attestationId = new BigInteger(attestationIdStr); + if (!m.getValue().isSchemaValue()) + { + continue; + } + + asset.addAttribute(m.getKey(), m.getValue().getString()); } - else + + //now add expiry, issuer key and valid from + MemberData validFrom = additionalMembers.get(VALID_FROM); + MemberData validTo = additionalMembers.get(VALID_TO); + + addDateToAttributes(asset, validFrom, R.string.valid_from, ctx); + addDateToAttributes(asset, validTo, R.string.valid_until, ctx); + } + + private void addDateToAttributes(NFTAsset asset, MemberData validFrom, int resource, Context ctx) + { + if (validFrom != null) { - attestationId = BigInteger.ZERO; + String dateFormat = "HH:mm dd MMM yy"; + SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat, Locale.ENGLISH); + + long validTime = validFrom.getValue().longValue() * 1000L; + if (validTime > 0) + { + String date = dateFormatter.format(validTime); + asset.addAttribute(ctx.getString(resource), date); + } } + } - isValid = rAtt.isValid(); + private void patchLegacyAttestation(RealmAttestation rAtt) + { + if (additionalMembers.isEmpty()) + { + BigInteger id = recoverId(rAtt); + MemberData tId = new MemberData(TICKET_ID, id.longValue()); + tId.setIsSchema(); + additionalMembers.put(TICKET_ID, tId); + } } - public String getDatabaseKey() + // For legacy attestation support + private BigInteger recoverId(RealmAttestation rAtt) { - //This should not be required: attestation shouldn't be able to not have an ID - BigInteger id = BigInteger.ZERO; - if (attestationId != null && attestationId.compareTo(BigInteger.ZERO) > 0) + BigInteger id; + try { - id = attestationId; + int index = rAtt.getAttestationKey().lastIndexOf("-"); + id = new BigInteger(rAtt.getAttestationKey().substring(index + 1)); } - return attestationDatabaseKey(tokenInfo.chainId, tokenInfo.address, id); + catch (Exception e) + { + // + id = BigInteger.ONE; //not really important + } + + return id; + } + + public void populateRealmAttestation(RealmAttestation rAtt) + { + rAtt.setSubTitle(generateMembersJSON()); + rAtt.setAttestation(attestation); + rAtt.setChain(tokenInfo.chainId); + rAtt.setName(tokenInfo.name); + } + + private String generateMembersJSON() + { + JSONArray members = new JSONArray(); + for (Map.Entry t : additionalMembers.entrySet()) + { + members.put(t.getValue().element); + } + + return members.toString(); + } + + public String getDatabaseKey() + { + //pull IDs from the members + return attestationDatabaseKey(tokenInfo.chainId, tokenInfo.address, getAttestationUID()); } public void setBaseTokenType(ContractType baseType) @@ -137,4 +288,193 @@ public ContractType getBaseTokenType() { return baseTokenType; } + + private void addToMemberData(String name, Type type) + { + additionalMembers.put(name, new MemberData(name, type)); + } + + private void populateMembersFromJSON(String jsonData) + { + try + { + JSONArray elements = new JSONArray(jsonData); + int index; + for (index = 0; index < elements.length(); index++) + { + JSONObject element = elements.getJSONObject(index); + additionalMembers.put(element.getString("name"), new MemberData(element)); + } + } + catch (Exception e) + { + Timber.e(e); + } + } + + private static class MemberData + { + JSONObject element; + + public MemberData(String name, Type type) + { + try + { + element = new JSONObject(); + element.put("name", name); + element.put("type", type.getTypeAsString()); + element.put("value", type.getValue()); + element.put("isSchema", true); + } + catch (Exception e) + { + Timber.e(e); + } + } + + public MemberData(String name, long value) + { + try + { + element = new JSONObject(); + element.put("name", name); + element.put("type", "uint"); + element.put("value", value); + } + catch (Exception e) + { + Timber.e(e); + } + } + + public MemberData(String name, String value) + { + try + { + element = new JSONObject(); + element.put("name", name); + element.put("type", "string"); + element.put("value", value); + } + catch (Exception e) + { + Timber.e(e); + } + } + + public MemberData(String name, boolean value) + { + try + { + element = new JSONObject(); + element.put("name", name); + element.put("type", "boolean"); + element.put("value", value); + } + catch (Exception e) + { + Timber.e(e); + } + } + + public MemberData(JSONObject jsonObject) + { + element = jsonObject; + } + + public String getEncoding() + { + return element.toString(); + } + + public BigInteger getValue() + { + try + { + String type = element.getString("type"); + if (type.startsWith("uint") || type.startsWith("int")) + { + return BigInteger.valueOf(element.getLong("value")); + } + } + catch (Exception e) + { + Timber.e(e); + } + + return BigInteger.ZERO; + } + + public String getString() + { + try + { + return element.getString("value"); + } + catch (Exception e) + { + Timber.e(e); + } + + return ""; + } + + public boolean isTrue() + { + try + { + return element.getBoolean("value"); + } + catch (Exception e) + { + Timber.e(e); + } + + return false; + } + + public boolean isSchema() + { + try + { + return element.getBoolean("isSchema"); + } + catch (Exception e) + { + Timber.e(e); + } + + return false; + } + + public void setIsSchema() + { + try + { + element.put("isSchema", true); + } + catch (Exception e) + { + Timber.e(e); + } + } + + public boolean isSchemaValue() + { + //true if this is integer or string + try + { + return element.getBoolean("isSchema") + && (element.getString("type").startsWith("uint") + || element.getString("type").startsWith("int") + || element.getString("type").equals("string")); + } + catch (Exception e) + { + Timber.e(e); + } + + return false; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index 0c6acb3d7b..312fa7e800 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -1088,4 +1088,9 @@ public HashSet processLogsAndStoreTransferEvents(EthLog receiveLogs, { return null; } + + public void addAssetElements(NFTAsset asset, Context ctx) + { + + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java index a1343e9fdc..b10a4608d1 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java @@ -43,22 +43,37 @@ public class TokenCardMeta implements Comparable, Parcelable public TokenCardMeta(long chainId, String tokenAddress, String balance, long timeStamp, AssetDefinitionService svs, String name, String symbol, ContractType type, TokenGroup group) { - this(chainId, tokenAddress, balance, timeStamp, svs, name, symbol, type, group, BigInteger.ZERO); + this(chainId, tokenAddress, balance, timeStamp, svs, name, symbol, type, group, ""); } - public TokenCardMeta(long chainId, String tokenAddress, String balance, long timeStamp, AssetDefinitionService svs, String name, String symbol, ContractType type, TokenGroup group, BigInteger attnId) + public TokenCardMeta(long chainId, String tokenAddress, String balance, long timeStamp, AssetDefinitionService svs, String name, String symbol, ContractType type, TokenGroup group, String attnId) { this.tokenId = TokensRealmSource.databaseKey(chainId, tokenAddress) - + (attnId.compareTo(BigInteger.ZERO) > 0 ? ("-" + attnId) : "") - + (group == TokenGroup.ATTESTATION ? "-att" : ""); + + (!TextUtils.isEmpty(attnId) ? ("-" + attnId) : "") + (group == TokenGroup.ATTESTATION ? "-att" : ""); this.lastUpdate = timeStamp; this.type = type; this.balance = balance; - this.nameWeight = calculateTokenNameWeight(chainId, tokenAddress, svs, name, symbol, isEthereum(), group, attnId); + this.nameWeight = calculateTokenNameWeight(chainId, tokenAddress, svs, name, symbol, isEthereum(), group, attnId.hashCode()); this.filterText = symbol + "'" + name; this.group = group; } + public String getAttestationId() + { + //should end with -att + if (tokenId.endsWith("-att")) + { + int sepIndex = tokenId.indexOf("-"); + sepIndex = tokenId.indexOf("-", sepIndex+1); + String attnId = tokenId.substring(sepIndex+1, tokenId.length() - 4); + return attnId; + } + else + { + return ""; + } + } + public TokenCardMeta(long chainId, String tokenAddress, String balance, long timeStamp, long lastTxUpdate, ContractType type, TokenGroup group) { this.tokenId = TokensRealmSource.databaseKey(chainId, tokenAddress) + (group == TokenGroup.ATTESTATION ? "-att" : ""); @@ -78,7 +93,7 @@ public TokenCardMeta(Token token, String filterText) this.lastTxUpdate = token.lastTxCheck; this.type = token.getInterfaceSpec(); this.balance = token.balance.toString(); - this.nameWeight = calculateTokenNameWeight(token.tokenInfo.chainId, token.tokenInfo.address, null, token.getName(), token.getSymbol(), isEthereum(), token.group, BigInteger.ZERO); + this.nameWeight = calculateTokenNameWeight(token.tokenInfo.chainId, token.tokenInfo.address, null, token.getName(), token.getSymbol(), isEthereum(), token.group, 0); this.filterText = filterText; this.group = token.group; this.isEnabled = TextUtils.isEmpty(filterText) || !filterText.equals(CHECK_MARK); @@ -183,7 +198,7 @@ public BigInteger getTokenID() } } - private long calculateTokenNameWeight(long chainId, String tokenAddress, AssetDefinitionService svs, String tokenName, String symbol, boolean isEth, TokenGroup group, BigInteger attnId) + private long calculateTokenNameWeight(long chainId, String tokenAddress, AssetDefinitionService svs, String tokenName, String symbol, boolean isEth, TokenGroup group, int attnId) { int weight = 1000; //ensure base eth types are always displayed first String name = svs != null ? svs.getTokenName(chainId, tokenAddress, 1) : null; @@ -232,7 +247,7 @@ else if (isEth) if (group == TokenGroup.ATTESTATION) { - weight += (attnId.longValue() + 1); + weight += (attnId + 1); } return weight; diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java b/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java index ea1902c209..d0e92cb200 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenLocalSource.java @@ -91,7 +91,7 @@ Single fetchAllTokenMetas(Wallet wallet, List networkFilt void updateTicker(long chainId, String address, TokenTicker ticker); Single storeTokenInfo(Wallet wallet, TokenInfo tInfo, ContractType type); - Token fetchAttestation(long chainId, Wallet wallet, String address, BigInteger tokenId); + Token fetchAttestation(long chainId, Wallet wallet, String address, String attnId); List fetchAttestations(long chainId, String walletAddress, String tokenAddress); } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java index 5c8a6eecfb..b58748186e 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -281,10 +281,10 @@ public Token fetchToken(long chainId, String walletAddress, String address) } @Override - public Token fetchAttestation(long chainId, String walletAddress, String address, BigInteger tokenId) + public Token fetchAttestation(long chainId, String walletAddress, String address, String attnId) { Wallet wallet = new Wallet(walletAddress); - return localSource.fetchAttestation(chainId, wallet, address, tokenId); + return localSource.fetchAttestation(chainId, wallet, address, attnId); } @Override diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java index aa659129f8..fd232f4f87 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepositoryType.java @@ -102,7 +102,7 @@ Single fetchAllTokenMetas(Wallet wallet, List networkFilt Single storeTokenInfo(Wallet wallet, TokenInfo tInfo, ContractType type); - Token fetchAttestation(long chainId, String currentAddress, String toLowerCase, BigInteger tokenId); + Token fetchAttestation(long chainId, String currentAddress, String toLowerCase, String attnId); List fetchAttestations(long chainId, String walletAddress, String tokenAddress); } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java index 6ac5dd59a2..422634c1d7 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java @@ -95,9 +95,14 @@ public static String eventBlockKey(long chainId, String eventAddress, String nam return eventAddress.toLowerCase() + "-" + chainId + "-" + namedType + "-" + filter + "-eventBlock"; } - public static String attestationDatabaseKey(long chainId, String address, BigInteger tokenId) + public static String attestationDatabaseKey(long chainId, String address, String attnId) { - return address.toLowerCase() + "-" + chainId + "-" + tokenId.toString(); + return address.toLowerCase() + "-" + chainId + "-" + attnId; + } + + public static String attestationDatabaseKey(long chainId, String address, BigInteger conferenceId, BigInteger ticketId) + { + return address.toLowerCase() + "-" + chainId + "-" + conferenceId.toString() + "-" + ticketId.toString(); } public static String convertStringBalance(String balance, ContractType type) @@ -303,22 +308,28 @@ public Token fetchToken(long chainId, Wallet wallet, String address) } } + private Token fetchAttestation(long chainId, Wallet wallet, RealmAttestation rAttn) + { + Token token = fetchToken(chainId, wallet, rAttn.getTokenAddress()); + TokenInfo tInfo = token != null ? token.tokenInfo : Utils.getDefaultAttestationInfo(chainId); + Attestation att = new Attestation(tInfo, ethereumNetworkRepository.getNetworkByChain(chainId).getShortName(), rAttn.getAttestation()); + att.setTokenWallet(wallet.address); + att.loadAttestationData(rAttn); + return att; + } + @Override - public Token fetchAttestation(long chainId, Wallet wallet, String address, BigInteger tokenId) + public Token fetchAttestation(long chainId, Wallet wallet, String address, String attnId) { - Token token = fetchToken(chainId, wallet, address); try (Realm realm = realmManager.getRealmInstance(wallet)) { RealmAttestation realmAttestation = realm.where(RealmAttestation.class) - .equalTo("address", attestationDatabaseKey(chainId, address, tokenId)) + .equalTo("address", attestationDatabaseKey(chainId, address, attnId)) .findFirst(); if (realmAttestation != null) { - Attestation att = new Attestation(token.tokenInfo, token.getNetworkName(), realmAttestation.getAttestation()); - att.setTokenWallet(wallet.address); - att.loadAttestationData(realmAttestation); //TODO: Store issuer, expiry dates etc in Realm - return att; + return fetchAttestation(chainId, wallet, realmAttestation); } } @@ -1027,10 +1038,10 @@ private Single joinAttestations(Wallet wallet, TokenCardMeta[] if (rAtt.supportsChain(networkFilters)) { long chainId = rAtt.getChains().get(0); - Token token = fetchToken(chainId, wallet, rAtt.getTokenAddress()); + Attestation attn = (Attestation) fetchAttestation(chainId, wallet, rAtt); TokenCardMeta tcmAttestation = new TokenCardMeta(chainId, rAtt.getTokenAddress(), "1", System.currentTimeMillis(), - svs, rAtt.getName(), token.tokenInfo.symbol, token.getInterfaceSpec(), TokenGroup.ATTESTATION, new BigInteger(rAtt.getId())); + svs, rAtt.getName(), attn.tokenInfo.symbol, attn.getBaseTokenType(), TokenGroup.ATTESTATION, attn.getAttestationUID()); tcmAttestation.isEnabled = true; metaList.add(tcmAttestation); } diff --git a/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java b/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java index e62c3e8428..38d515e85c 100644 --- a/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java +++ b/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java @@ -34,7 +34,8 @@ public void setName(String name) { this.name = name; } - public String getTokenAddress() { + public String getTokenAddress() + { String tAddress = address; if (tAddress.contains("-")) { @@ -46,6 +47,11 @@ public String getTokenAddress() { } } + public String getAttestationKey() + { + return address; + } + public String getSubTitle() { return subTitle; diff --git a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java index 175df8bd10..265e8c696f 100644 --- a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java +++ b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java @@ -74,7 +74,8 @@ public void openAttestation(Activity context, long chainId, String address, Wall intent.putExtra(C.Key.WALLET, wallet); intent.putExtra(C.EXTRA_CHAIN_ID, chainId); intent.putExtra(C.EXTRA_ADDRESS, address); - intent.putExtra(C.EXTRA_TOKEN_ID, asset.getTokenIdStr()); + intent.putExtra(C.EXTRA_TOKEN_ID, "1"); + intent.putExtra(C.EXTRA_ATTESTATION_ID, asset.getAttestationID()); intent.putExtra(C.EXTRA_NFTASSET, asset); context.startActivityForResult(intent, C.TERMINATE_ACTIVITY); } diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index 37989eecf7..8989ea5aa3 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -3,6 +3,9 @@ import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; import static com.alphawallet.app.repository.TokensRealmSource.IMAGES_DB; import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; +import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_ID; import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_CURRENT_SCHEMA; import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_REPO_SERVER; import static com.alphawallet.token.tools.TokenDefinition.UNCHANGED_SCRIPT; @@ -76,8 +79,17 @@ import org.web3j.abi.FunctionEncoder; import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.Bytes; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.DynamicStruct; import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.StaticStruct; import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes32; +import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; @@ -100,6 +112,7 @@ import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -2595,9 +2608,9 @@ public Observable resolveAttrs(Token token, TokenDe return resolveAttrs(token, tokenId, definition, attrList, itemView); } - public TokenScriptResult.Attribute getAvailableAttestation(Token token, TSAction action, BigInteger tokenId) + public TokenScriptResult.Attribute getAvailableAttestation(Token token, TSAction action, String attnId) { - Attestation att = (action != null && action.modifier == ActionModifier.ATTESTATION) ? (Attestation) tokensService.getAttestation(token.tokenInfo.chainId, token.getAddress(), tokenId) : null; + Attestation att = (action != null && action.modifier == ActionModifier.ATTESTATION) ? (Attestation) tokensService.getAttestation(token.tokenInfo.chainId, token.getAddress(), attnId) : null; if (att != null) { return new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, Numeric.toHexString(att.getAttestation())); @@ -2608,9 +2621,9 @@ public TokenScriptResult.Attribute getAvailableAttestation(Token token, TSAction } } - public Map getAttestationFunctionMap(long chainId, String address, BigInteger tokenId) + public Map getAttestationFunctionMap(long chainId, String address, String attnId) { - Token att = tokensService.getAttestation(chainId, address, tokenId); + Token att = tokensService.getAttestation(chainId, address, attnId); TokenDefinition td = getAssetDefinition(chainId, address); Map actions = new HashMap<>(); if (att != null && td != null) @@ -2998,52 +3011,153 @@ public Attestation validateAttestation(String attestation, TokenInfo tInfo) return att; } - public Attestation validateAttestation(EasAttestation attestation) + public Attestation validateAttestation(EasAttestation attestation, String importedAttestation) { - Attestation att = null; - //1. Resolve UID. For now, just use default: This should be on a switch for chains - String defaultUID = "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"; String recoverAttestationSigner = recoverSigner(attestation); //1. Validate signer via key attestation service (using UID). - //2. Decode the ABI encoded payload. - //3. + //call validate + SchemaRecord schemaRecord = fetchSchemaRecord(attestation.getChainId(), getKeySchemaUID(attestation.getChainId())); + boolean attestationValid = checkAttestationIssuer(schemaRecord, attestation.getChainId(), recoverAttestationSigner); + + //2. Decode the ABI encoded payload to pull out the info. ABI Decode the schema bytes + //initially we need a hardcoded schema - this should be fetched from the schema record EAS contract + //fetch the schema of the attestation + SchemaRecord attestationSchema = fetchSchemaRecord(attestation.getChainId(), attestation.schema); + //convert into functionDecode + List names = new ArrayList<>(); + List values = decodeAttestationData(attestation.data, attestationSchema.schema, names); + + NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(attestation.getChainId()); + //For EAS attestation use the EAS contract as the token base + TokenInfo tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId()); + Attestation localAttestation = new Attestation(tInfo, networkInfo.name, importedAttestation.getBytes(StandardCharsets.UTF_8)); + + localAttestation.handleEASAttestation(attestation, names, values, attestationValid); + localAttestation.setTokenWallet(tokensService.getCurrentAddress()); + return localAttestation; + } + + private List decodeAttestationData(String attestationData, String decodeSchema, List names) + { + List> returnTypes = new ArrayList>(); + //build decoder + String[] typeData = decodeSchema.split(","); + for (String typeElement : typeData) + { + String[] data = typeElement.split(" "); + String type = data[0]; + String name = data[1]; + if (type.startsWith("uint") || type.startsWith("int")) + { + type = "uint"; + } + else if (type.startsWith("bytes") && !type.equals("bytes")) + { + type = "bytes32"; + } - // + TypeReference tRef = null; + switch (type) + { + case "uint": + tRef = new TypeReference() { }; + break; + case "address": + tRef = new TypeReference
() { }; + break; + case "bytes32": + tRef = new TypeReference() { }; + break; + case "string": + tRef = new TypeReference() { }; + break; + case "bytes": + tRef = new TypeReference() { }; + break; + case "bool": + tRef = new TypeReference() { }; + break; + default: + break; + } + if (tRef != null) + { + returnTypes.add(tRef); + } + else + { + Timber.e("Unhandled type!"); + returnTypes.add(new TypeReference() { }); + } + names.add(name); + } + //decode the schema and populate the Attestation element + return FunctionReturnDecoder.decode(attestationData, org.web3j.abi.Utils.convert(returnTypes)); + } + public static class SchemaRecord extends DynamicStruct + { + public byte[] uid; + public Address resolver; + public boolean revocable; + public String schema; + + public SchemaRecord(byte[] uid, Address resolver, boolean revocable, String schema) { + super( + new org.web3j.abi.datatypes.generated.Bytes32(uid), + new org.web3j.abi.datatypes.Address(resolver.getValue()), + new org.web3j.abi.datatypes.Bool(revocable), + new org.web3j.abi.datatypes.Utf8String(schema)); + this.uid = uid; + this.resolver = resolver; + this.revocable = revocable; + this.schema = schema; + } - /*NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(tInfo.chainId); - att = new Attestation(tInfo, networkInfo.name, Numeric.hexStringToByteArray(attestation)); - att.setTokenWallet(tokensService.getCurrentAddress()); + public SchemaRecord(Bytes32 uid, Address resolver, Bool revocable, Utf8String schema) { + super(uid, resolver, revocable, schema); + this.uid = uid.getValue(); + this.resolver = resolver; + this.revocable = revocable.getValue(); + this.schema = schema.getValue(); + } + } - //call validation function and get details - TokenDefinition.Attestation definitionAtt = td.getAttestation(); - //can we get the details? + private SchemaRecord fetchSchemaRecord(long chainId, String schemaUID) + { + //1. Resolve UID. For now, just use default: This should be on a switch for chains + String globalResolver = getEASSchemaContract(chainId); - if (definitionAtt != null && definitionAtt.function != null) - { - //pull return type - FunctionDefinition fd = definitionAtt.function; - //add attestation to attr map - //call function - org.web3j.abi.datatypes.Function transaction = tokenscriptUtility.generateTransactionFunction(att, BigInteger.ZERO, td, fd, this); - transaction = new Function(fd.method, transaction.getInputParameters(), td.getAttestationReturnTypes()); //set return types + //format transaction to get key resolver + Function getKeyResolver2 = new Function("getSchema", + Collections.singletonList(new Bytes32(Numeric.hexStringToByteArray(schemaUID))), + Collections.singletonList(new TypeReference() {})); - //call and handle result - String result = tokenscriptUtility.callSmartContract(tInfo.chainId, tInfo.address, transaction); + String result = tokenscriptUtility.callSmartContract(chainId, globalResolver, getKeyResolver2); + List values = FunctionReturnDecoder.decode(result, getKeyResolver2.getOutputParameters()); - //break down result - List values = FunctionReturnDecoder.decode(result, transaction.getOutputParameters()); + return (SchemaRecord)values.get(0); + } - //interpret these values - att.handleValidation(td.getValidation(values)); - }*/ + private boolean checkAttestationIssuer(SchemaRecord schemaRecord, long chainId, String signer) + { + String rootKeyUID = getDefaultRootKeyUID(chainId); + //pull the key resolver + Address resolverAddr = schemaRecord.resolver; + //call the resolver to test key validity + Function validateKey = new Function("validateSignature", + Arrays.asList((new Bytes32(Numeric.hexStringToByteArray(rootKeyUID))), + new Address(signer)), + Collections.singletonList(new TypeReference() {})); - return att; + String result = tokenscriptUtility.callSmartContract(chainId, resolverAddr.getValue(), validateKey); + List values = FunctionReturnDecoder.decode(result, validateKey.getOutputParameters()); + return ((Bool)values.get(0)).getValue(); } private String recoverSigner(EasAttestation attestation) @@ -3070,4 +3184,91 @@ private String recoverSigner(EasAttestation attestation) return recoveredAddress; } + + // NB Java 11 doesn't have support for switching on a 'long' :( + public static String getEASContract(long chainId) + { + if (chainId == MAINNET_ID) + { + return "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587"; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"; + } + else + { + //Support Optimism Goerli (0xC2679fBD37d54388Ce493F1DB75320D236e1815e) + return ""; + } + } + + private String getEASSchemaContract(long chainId) + { + if (chainId == MAINNET_ID) + { + return "0xA7b39296258348C78294F95B872b282326A97BDF"; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0xA310da9c5B885E7fb3fbA9D66E9Ba6Df512b78eB"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } + + //UID of schema used for keys on each chain - the resolver is tied to this UID + private String getKeySchemaUID(long chainId) + { + if (chainId == MAINNET_ID) + { + return ""; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return ""; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } + + // If not specified + private String getDefaultRootKeyUID(long chainId) + { + if (chainId == MAINNET_ID) + { + return ""; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return ""; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0xee99de42f544fa9a47caaf8d4a4426c1104b6d7a9df7f661f892730f1b5b1e23"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/service/TokensService.java b/app/src/main/java/com/alphawallet/app/service/TokensService.java index a3c6d75793..40197380f0 100644 --- a/app/src/main/java/com/alphawallet/app/service/TokensService.java +++ b/app/src/main/java/com/alphawallet/app/service/TokensService.java @@ -1242,10 +1242,10 @@ public Token getToken(String walletAddress, long chainId, String tokenAddress) else return tokenRepository.fetchToken(chainId, walletAddress, tokenAddress.toLowerCase()); } - public Token getAttestation(long chainId, String addr, BigInteger tokenId) + public Token getAttestation(long chainId, String addr, String attnId) { //fetch attestation - return tokenRepository.fetchAttestation(chainId, currentAddress, addr.toLowerCase(), tokenId); + return tokenRepository.fetchAttestation(chainId, currentAddress, addr.toLowerCase(), attnId); } public List getAttestations(long chainId, String address) diff --git a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java index a7f9d50d6a..26c1ef9100 100644 --- a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java @@ -131,7 +131,6 @@ private void initViews() { tokenIds = token.stringHexToBigIntegerList(tokenIdStr); tokenId = tokenIds.get(0); tokenView = findViewById(R.id.web3_tokenview); - tokenView.setChainId(token.tokenInfo.chainId); tokenView.setWalletAddress(new Address(token.getWallet())); tokenView.setupWindowCallback(this); @@ -189,7 +188,7 @@ private void getAttrs() action = functions.get(actionMethod); List localAttrs = (action != null && action.attributes != null) ? new ArrayList<>(action.attributes.values()) : null; - TokenScriptResult.Attribute attestation = viewModel.getAssetDefinitionService().getAvailableAttestation(token, action, tokenId); + TokenScriptResult.Attribute attestation = viewModel.getAssetDefinitionService().getAvailableAttestation(token, action, getIntent().getStringExtra(C.EXTRA_ATTESTATION_ID)); if (attestation != null) { onAttr(attestation); diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index e1ea6b2499..db1bd793ba 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -256,7 +256,7 @@ private Token resolveAssetToken() { if (asset != null && asset.isAttestation()) { - return viewModel.getTokenService().getAttestation(chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS), tokenId); + return viewModel.getTokenService().getAttestation(chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS), asset.getAttestationID()); } else { @@ -593,6 +593,13 @@ private void setupAttestation() { tokenImage.setImageResource(R.drawable.zero_one_block); progressBar.setVisibility(View.GONE); + NFTAsset attnAsset = new NFTAsset(); + token.addAssetElements(attnAsset, this); + tivTokenId.setVisibility(View.GONE); + tokenDescription.setVisibility(View.GONE); + + //now populate + nftAttributeLayout.bind(token, attnAsset); } /** diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java index a1f861069e..a82e81fbd8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NFTAssetsAdapter.java @@ -90,7 +90,7 @@ public void attachAttestations(Token[] attestations) { Attestation thisAttn = (Attestation)att; NFTAsset attestationAsset = new NFTAsset(thisAttn); - displayData.add(new Pair<>(thisAttn.getAttestationId(), attestationAsset)); + //displayData.add(new Pair<>(thisAttn.getAttestationUID(), attestationAsset)); } sortData(); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java index 07b1c31be9..ecdbfde41c 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java @@ -17,7 +17,6 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.entity.walletconnect.WalletConnectSessionItem; -import com.alphawallet.app.repository.TokensMappingRepository; import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.TokensService; @@ -392,12 +391,10 @@ public SortedItem removeToken(long chainId, String tokenAddress) return null; } - //TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), - // assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationId()); - // tcmAttestation.isEnabled = true; public SortedItem removeAttestation(Token token) { Attestation attn = (Attestation)token; + String attnKey = attn.getDatabaseKey().toLowerCase(); for (int i = 0; i < items.size(); i++) { Object si = items.get(i); @@ -405,9 +402,8 @@ public SortedItem removeAttestation(Token token) { TokenSortedItem tsi = (TokenSortedItem) si; TokenCardMeta thisToken = tsi.value; - //Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getTokenID()); - if (thisToken.getTokenID().compareTo(attn.getAttestationId()) == 0 && thisToken.getAddress().equalsIgnoreCase(token.getAddress()) - && thisToken.getChain() == token.tokenInfo.chainId) + + if (thisToken.tokenId.toLowerCase().startsWith(attnKey)) { return items.removeItemAt(i); } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java index c6d794096d..dd6e0b8cd3 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java @@ -104,8 +104,19 @@ public void bind(@Nullable TokenCardMeta data, @NonNull Bundle addition) } try { + tokenLayout.setVisibility(View.VISIBLE); token = tokensService.getToken(data.getChain(), data.getAddress()); - if (token == null) + if (token != null) + { + token.group = data.getTokenGroup(); + } + + if (data.group == TokenGroup.ATTESTATION) + { + handleAttestation(data); + return; + } + else if (token == null) { fillEmpty(); return; @@ -117,16 +128,6 @@ else if (data.getNameWeight() < 1000L && !token.isEthereum()) if (backupChain != null) token = backupChain; } - token.group = data.getTokenGroup(); - - tokenLayout.setVisibility(View.VISIBLE); - - if (data.group == TokenGroup.ATTESTATION) - { - handleAttestation(data); - return; - } - if (EthereumNetworkRepository.isPriorityToken(token)) { extendedInfo.setVisibility(View.GONE); @@ -182,18 +183,18 @@ public void onDestroyView() private void handleAttestation(TokenCardMeta data) { - Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getTokenID()); - balanceEth.setText(shortTitle()); - BigInteger attestationId = attestation.getAttestationId(); - if (attestationId.compareTo(BigInteger.ZERO) > 0) + Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getAttestationId()); + //TODO: Take name from schema data if available + if (token != null) { - balanceCoin.setText(getString(R.string.valueSymbol, "AttestationId", attestationId.toString())); + balanceEth.setText(shortTitle()); } else { - balanceCoin.setText(getString(R.string.valueSymbol, "1", token.getSymbol())); + balanceEth.setText(attestation.tokenInfo.name); } - + //BigInteger attestationId = attestation.getAttestationUID(); + balanceCoin.setText(attestation.getAttestationDescription()); balanceCoin.setVisibility(View.VISIBLE); tokenIcon.setIsAttestation(attestation.getSymbol(), data.getChain()); token = attestation; diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 90e830a215..6cc9744904 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -1,5 +1,6 @@ package com.alphawallet.app.util; +import static com.alphawallet.app.service.AssetDefinitionService.getEASContract; import static com.alphawallet.ethereum.EthereumNetworkBase.AVALANCHE_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.BINANCE_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.CLASSIC_ID; @@ -31,6 +32,7 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.util.pattern.Patterns; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.ProviderTypedData; @@ -1114,6 +1116,22 @@ public static boolean hasAttestation(String url) } else { + //detect a base64 attestation + try + { + byte[] tryBase64Data = Base64.decode(url, Base64.DEFAULT); //is this a base64 string? + + if (tryBase64Data.length > 0 ) + { + return true; + } + } + catch (IllegalArgumentException e) + { + // no action, return false; + Timber.w(e); + } + return false; } } @@ -1121,21 +1139,24 @@ public static boolean hasAttestation(String url) public static String getAttestationString(String url) { int hashIndex = url.indexOf("#attestation="); - if (hashIndex >= 0) + String decoded; + if (hashIndex >= 0) //EAS style attestations have the magic link style { url = url.substring(hashIndex + 13); + decoded = URLDecoder.decode(url, StandardCharsets.UTF_8); } - try - { - String decoded = URLDecoder.decode(url, StandardCharsets.UTF_8.name()); - Timber.d("decoded url: " + decoded); - return decoded; - } - catch (UnsupportedEncodingException e) + else { - Timber.e(e); - return ""; + decoded = url; } + + Timber.d("decoded url: %s", decoded); + return decoded; + } + + public static TokenInfo getDefaultAttestationInfo(long chainId) + { + return new TokenInfo(getEASContract(chainId), "EAS Attestation", "ATTN", 0, true, chainId); } public static String decompress(String url) @@ -1177,16 +1198,12 @@ private static String toAttestationJson(String jsonString) Long.parseLong(e[15]) ); - String attestationJson = new Gson().toJson(easAttestation); - - return attestationJson; + return new Gson().toJson(easAttestation); } public static String inflateData(String deflatedData) { - byte[] deflatedBytes; - - deflatedBytes = Base64.decode(deflatedData, Base64.DEFAULT); + byte[] deflatedBytes = Base64.decode(deflatedData, Base64.DEFAULT); Inflater inflater = new Inflater(); inflater.setInput(deflatedBytes); @@ -1208,9 +1225,7 @@ public static String inflateData(String deflatedData) inflatedData = outputStream.toByteArray(); // Convert the inflated bytes to a string - String inflatedString = new String(inflatedData); - - return inflatedString; + return new String(inflatedData); } catch (Exception e) { diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index a6aef6324b..523351c961 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -586,7 +586,7 @@ private void completeImport(QRResult attestation, Attestation tokenAttn) if (tokenAttn.isValid() == AttestationValidationStatus.Pass) { TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), - assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationId()); + assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); tcmAttestation.isEnabled = true; updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); } @@ -634,10 +634,7 @@ private Single storeAttestationInternal(QRResult attestation, Token realmAttn = r.createObject(RealmAttestation.class, key); } - realmAttn.setAttestation(attestation.getAttestation()); - realmAttn.setChain(tInfo.chainId); - realmAttn.setName(tInfo.name); - realmAttn.setId(attn.getAttestationId().toString()); + attn.populateRealmAttestation(realmAttn); }); } catch (Exception e) @@ -675,7 +672,7 @@ public void importEASAttestation(QRResult qrAttn) EasAttestation easAttn = new Gson().fromJson(qrAttn.functionDetail, EasAttestation.class); //validation UID: - storeAttestation(easAttn) + storeAttestation(easAttn, qrAttn.functionDetail) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(attn -> completeImport(easAttn, attn), this::onError) @@ -683,33 +680,62 @@ public void importEASAttestation(QRResult qrAttn) } @SuppressWarnings("checkstyle:MissingSwitchDefault") - private Single storeAttestation(EasAttestation attestation) + private Single storeAttestation(EasAttestation attestation, String importedAttestation) { //Use Default key unless specified - Attestation attn = assetDefinitionService.validateAttestation(attestation); - switch (attn.isValid()) + return Single.fromCallable(() -> { + Attestation attn = assetDefinitionService.validateAttestation(attestation, importedAttestation); + switch (attn.isValid()) + { + case Pass: + return storeAttestationInternal(attestation, attn, importedAttestation); + case Expired: + case Issuer_Not_Valid: + case Incorrect_Subject: + attestationError.postValue(attn.isValid().getValue()); + break; + } + + return attn; + }); + } + + private Attestation storeAttestationInternal(EasAttestation attestation, Attestation attn, String importedAttestation) + { + try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue())) { - case Pass: - //return storeAttestationInternal(attestation, attn); - case Expired: - case Issuer_Not_Valid: - case Incorrect_Subject: - attestationError.postValue(attn.isValid().getValue()); - break; - } + realm.executeTransaction(r -> { + String key = attn.getDatabaseKey(); + RealmAttestation realmAttn = r.where(RealmAttestation.class) + .equalTo("address", key) + .findFirst(); - return Single.fromCallable(() -> attn); + if (realmAttn == null) + { + realmAttn = r.createObject(RealmAttestation.class, key); + } + + realmAttn.setId(importedAttestation); + attn.populateRealmAttestation(realmAttn); + }); + } + catch (Exception e) + { + e.printStackTrace(); + } + return attn; } private void completeImport(EasAttestation attestation, Attestation tokenAttn) { - /*if (tokenAttn.isValid() == AttestationValidationStatus.Pass) + if (tokenAttn.isValid() == AttestationValidationStatus.Pass) { - TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), - assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationId()); + TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, tokenAttn.getAddress(), "1", System.currentTimeMillis(), + assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), + TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); tcmAttestation.isEnabled = true; updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); - }*/ + } } public void removeAttestation(Token token) diff --git a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java index 2a99e480d2..85fdd3e31f 100644 --- a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java +++ b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java @@ -178,8 +178,8 @@ public void setupAttestationFunctions(StandardFunctionInterface functionInterfac adapter = adp; selection.clear(); this.token = token; - functions = assetSvs.getAttestationFunctionMap(token.tokenInfo.chainId, token.getAddress(), ((Attestation)token).getAttestationId()); - selection.add(((Attestation)token).getAttestationId()); + functions = assetSvs.getAttestationFunctionMap(token.tokenInfo.chainId, token.getAddress(), ((Attestation)token).getAttestationUID()); + //selection.add(((Attestation)token).getAttestationUID()); assetService = assetSvs; showButtons = true; diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index db4e2e2d77..5930b25537 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -980,4 +980,6 @@ Ser notificado No recibirá notificaciones mientras AlphaWallet esté en segundo plano. Puede cambiar esto yendo a Configuración > Notificaciones Minted + Válido desde + Válido hasta diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3aa2b1d497..7edb92fd30 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -994,4 +994,6 @@ Recevez une notification Vous ne recevrez pas de notifications tant qu\'AlphaWallet est en arrière-plan. Vous pouvez changer cela en allant dans Paramètres > Notifications Minted + Valable à Compter + Valable Jusque diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index e3ad937305..ddf0937236 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -985,4 +985,6 @@ Dapatkan pemberitahuan Anda tidak akan menerima notifikasi saat AlphaWallet ada di latar belakang. Anda dapat mengubahnya dengan membuka Setelan > Notifikasi Minted + Berlaku dari tanggal + Berlaku hingga diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index bfb4ba648c..21c789542a 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -1015,4 +1015,6 @@ အကြောင်းကြားပါ။ AlphaWallet နောက်ခံတွင် ရှိနေစဉ်တွင် သင်သည် အကြောင်းကြားချက်များကို လက်ခံရရှိမည်မဟုတ်ပါ။ ဆက်တင်များ > အကြောင်းကြားချက်များသို့ သွားခြင်းဖြင့် ၎င်းကို ပြောင်းလဲနိုင်သည်။ Minted + မှ တရားဝင် + သည်အထိ အကျုံးဝင်သည်။ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 0ac7037def..718b909350 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -994,4 +994,6 @@ Nhận được thông báo Bạn sẽ không nhận được thông báo khi AlphaWallet ở chế độ nền. Bạn có thể thay đổi điều này bằng cách đi tới Cài đặt > Thông báo Minted + Có hiệu lực từ ngày + Có hiệu lực diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index df3ac6004c..efaa2069a4 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -981,4 +981,6 @@ 得到通知 当 AlphaWallet 处于后台时,您将不会收到通知。 您可以通过转到“设置”>“通知”来更改此设置 Minted + 有效起始日期 + 有效期至日期 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index feacd467db..f80e7a7c8f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1056,4 +1056,6 @@ Get notified You will not receive notifications while AlphaWallet is in the background. You can change this by going to Settings > Notifications Minted + Valid From + Valid Until From 6849b3b6a6b86f9496dcec8308b4965f36b406da Mon Sep 17 00:00:00 2001 From: James Brown Date: Sat, 1 Jul 2023 22:19:24 +0300 Subject: [PATCH 08/11] Update EAS handling --- app/build.gradle | 4 +- .../app/entity/EasAttestation.java | 57 ++++ .../attestation/AttestationCoreData.java | 56 ++++ .../app/entity/attestation/SchemaRecord.java | 37 +++ .../app/entity/nftassets/NFTAsset.java | 35 ++ .../app/entity/tokens/Attestation.java | 303 ++++++++++++++++-- .../alphawallet/app/entity/tokens/Token.java | 42 ++- .../app/entity/tokens/TokenCardMeta.java | 2 +- .../tokenscript/TokenscriptFunction.java | 11 + .../app/repository/TokensRealmSource.java | 10 +- .../app/service/AssetDefinitionService.java | 282 ++++++++++------ .../alphawallet/app/service/GasService.java | 15 + .../app/service/PriceAlertsService.java | 4 +- .../app/ui/AssetDisplayActivity.java | 6 +- .../alphawallet/app/ui/FunctionActivity.java | 42 ++- .../com/alphawallet/app/ui/NFTActivity.java | 2 +- .../app/ui/NFTAssetDetailActivity.java | 136 +++++--- .../alphawallet/app/ui/NFTAssetsFragment.java | 2 +- .../com/alphawallet/app/ui/SendActivity.java | 2 - .../com/alphawallet/app/ui/TokenActivity.java | 2 +- .../app/ui/TokenDetailActivity.java | 2 +- .../app/ui/TokenFunctionActivity.java | 2 +- .../app/ui/TokenScriptManagementActivity.java | 2 +- .../alphawallet/app/ui/WalletFragment.java | 18 +- .../adapter/NonFungibleTokenAdapter.java | 4 +- .../adapter/TokenScriptManagementAdapter.java | 11 +- .../app/ui/widget/adapter/TokensAdapter.java | 1 - .../app/ui/widget/holder/TokenHolder.java | 19 +- .../java/com/alphawallet/app/util/Utils.java | 36 ++- .../app/viewmodel/Erc20DetailViewModel.java | 2 +- .../app/viewmodel/HomeViewModel.java | 7 + .../app/viewmodel/NFTInfoViewModel.java | 2 +- .../app/viewmodel/NFTViewModel.java | 2 +- .../app/viewmodel/TokenFunctionViewModel.java | 40 ++- .../TokenScriptManagementViewModel.java | 10 +- .../app/viewmodel/WalletViewModel.java | 41 ++- .../alphawallet/app/web3/Web3TokenView.java | 2 +- .../app/widget/FunctionButtonBar.java | 4 +- .../com/alphawallet/app/widget/TokenIcon.java | 19 +- .../token/entity/TSOriginType.java | 3 +- .../token/tools/TokenDefinition.java | 119 +++++-- 41 files changed, 1108 insertions(+), 288 deletions(-) create mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/AttestationCoreData.java create mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/SchemaRecord.java diff --git a/app/build.gradle b/app/build.gradle index 1baf92f4de..621e2934ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,8 +85,8 @@ android { } } defaultConfig { - versionCode 242 - versionName "3.63" + versionCode 243 + versionName "3.64" applicationId "io.stormbird.wallet" minSdkVersion 24 diff --git a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java index f7458be047..30bc1cca0b 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java @@ -1,10 +1,15 @@ package com.alphawallet.app.entity; +import com.alphawallet.app.entity.attestation.AttestationCoreData; +import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.service.KeystoreAccountService; import com.alphawallet.token.tools.Numeric; import org.json.JSONArray; import org.json.JSONObject; +import org.web3j.abi.datatypes.Address; +import org.web3j.crypto.Sign; import java.math.BigInteger; @@ -101,6 +106,10 @@ public void setS(String s) public long getV() { + if (v == 0 || v == 1) + { + v += 27; + } return v; } @@ -223,6 +232,17 @@ public void setNonce(long nonce) this.nonce = nonce; } + public byte[] getSignatureBytes() + { + byte[] r = Numeric.hexStringToByteArray(getR()); + byte[] s = Numeric.hexStringToByteArray(getS()); + byte v = (byte)(getV() & 0xFF); + + Sign.SignatureData sig = new Sign.SignatureData(v, r, s); + + return KeystoreAccountService.bytesFromSignature(sig); + } + public String getEIP712Attestation() { JSONObject eip712 = new JSONObject(); @@ -311,4 +331,41 @@ private void putElement(JSONArray jsonType, String name, String type) throws Exc jsonType.put(element); } + + public AttestationCoreData getAttestationCore() + { + /* +verifyEASAttestation((bytes32,address,uint64,uint64,bool,bytes32,bytes),bytes) +struct AttestationCoreData { + bytes32 schema; // The UID of the associated EAS schema + address recipient; // The recipient of the attestation. + uint64 time; // The time when the attestation is valid from (Unix timestamp). + uint64 expirationTime; // The time when the attestation expires (Unix timestamp). + bool revocable; // Whether the attestation is revocable. + bytes32 refUID; // The UID of the related attestation. + bytes data; // The actual Schema data (eg eventId: 12345, ticketId: 6 etc) +} + */ + + /*return new AttestationCoreData(new Address(recipient), time, expirationTime, revocable, + Numeric.toBytesPadded(new BigInteger(refUID), 32), + Numeric.hexStringToByteArray(data), BigInteger.ZERO, + Numeric.hexStringToByteArray(schema));*/ + + BigInteger bi = new BigInteger(refUID); + + byte[] lala = Numeric.toBytesPadded(bi, 32); + + BigInteger bi2 = Numeric.toBigInt(schema); + + byte[] lala2 = Numeric.toBytesPadded(bi2, 32); + + Address l = new Address(recipient); + + byte[] bib = Numeric.hexStringToByteArray(data); + + return new AttestationCoreData(lala2, + new Address(recipient), time, expirationTime, revocable, lala, + bib); + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationCoreData.java b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationCoreData.java new file mode 100644 index 0000000000..84843862eb --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationCoreData.java @@ -0,0 +1,56 @@ +package com.alphawallet.app.entity.attestation; + +import com.alphawallet.token.tools.Numeric; + +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.DynamicBytes; +import org.web3j.abi.datatypes.DynamicStruct; +import org.web3j.abi.datatypes.generated.Bytes32; +import org.web3j.abi.datatypes.generated.Uint64; + +import java.math.BigInteger; + +import timber.log.Timber; + +public class AttestationCoreData extends DynamicStruct +{ + byte[] schema; + public Address recipient; + long time; + long expirationTime; + public boolean revocable; + byte[] refUID; + byte[] data; + + public AttestationCoreData(byte[] schema, Address recipient, long time, long expirationTime, boolean revocable, byte[] refUID, byte[] data) { + super( + new org.web3j.abi.datatypes.generated.Bytes32(schema), + new org.web3j.abi.datatypes.Address(recipient.getValue()), + new org.web3j.abi.datatypes.generated.Uint64(BigInteger.valueOf(time)), + new org.web3j.abi.datatypes.generated.Uint64(BigInteger.valueOf(expirationTime)), + new org.web3j.abi.datatypes.Bool(revocable), + new org.web3j.abi.datatypes.generated.Bytes32(refUID), + new org.web3j.abi.datatypes.DynamicBytes(data)); + this.recipient = recipient; + this.time = time; + this.expirationTime = expirationTime; + this.revocable = revocable; + this.refUID = refUID; + this.data = data; + this.schema = schema; + + Timber.d("Format for struct: " + Numeric.toHexString(schema) + "," + recipient.getValue() + "," + time + "," + expirationTime + "," + revocable + "," + Numeric.toHexString(refUID) + ",0," + Numeric.toHexString(data)); + } + + public AttestationCoreData(Bytes32 schema, Address recipient, Uint64 time, Uint64 expirationTime, Bool revocable, Bytes32 refUID, DynamicBytes data) { + super(schema, recipient, time, expirationTime, revocable, refUID, data); + this.recipient = recipient; + this.time = time.getValue().longValue(); + this.expirationTime = expirationTime.getValue().longValue(); + this.revocable = revocable.getValue(); + this.refUID = refUID.getValue(); + this.data = data.getValue(); + this.schema = schema.getValue(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/SchemaRecord.java b/app/src/main/java/com/alphawallet/app/entity/attestation/SchemaRecord.java new file mode 100644 index 0000000000..cd5eb30963 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/attestation/SchemaRecord.java @@ -0,0 +1,37 @@ +package com.alphawallet.app.entity.attestation; + +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.DynamicStruct; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes32; + +public class SchemaRecord extends DynamicStruct +{ + public byte[] uid; + public Address resolver; + public boolean revocable; + public String schema; + + public SchemaRecord(byte[] uid, Address resolver, boolean revocable, String schema) + { + super( + new org.web3j.abi.datatypes.generated.Bytes32(uid), + new org.web3j.abi.datatypes.Address(resolver.getValue()), + new org.web3j.abi.datatypes.Bool(revocable), + new org.web3j.abi.datatypes.Utf8String(schema)); + this.uid = uid; + this.resolver = resolver; + this.revocable = revocable; + this.schema = schema; + } + + public SchemaRecord(Bytes32 uid, Address resolver, Bool revocable, Utf8String schema) + { + super(uid, resolver, revocable, schema); + this.uid = uid.getValue(); + this.resolver = resolver; + this.revocable = revocable.getValue(); + this.schema = schema.getValue(); + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java index 0c53cb779c..6ddf0d2061 100644 --- a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java @@ -3,14 +3,17 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Pair; import androidx.annotation.Nullable; import com.alphawallet.app.entity.opensea.OpenSeaAsset; import com.alphawallet.app.entity.tokens.Attestation; import com.alphawallet.app.entity.tokens.ERC1155Token; +import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.util.Utils; +import com.alphawallet.token.tools.TokenDefinition; import org.json.JSONArray; import org.json.JSONException; @@ -589,6 +592,38 @@ public void addAttribute(String name, String value) attributeMap.put(name, value); } + public boolean setupScriptElements(TokenDefinition td) + { + boolean hasMetaData = false; + TokenDefinition.Attestation internalAtt = td != null ? td.getAttestation() : null; + if (internalAtt != null && internalAtt.metadata.size() > 0) + { + internalAtt.metadata.keySet().forEach(key -> assetMap.put(key, internalAtt.metadata.get(key))); + hasMetaData = true; + } + + return hasMetaData; + } + + public void setupScriptAttributes(TokenDefinition td, Token token) + { + TokenDefinition.Attestation internalAtt = td.getAttestation(); + if (internalAtt != null && internalAtt.attributes != null && internalAtt.attributes.size() > 0) + { + for (Map.Entry attr : internalAtt.attributes.entrySet()) + { + String typeName = attr.getKey(); + String attrTitle = attr.getValue(); + String attrValue = token.getAttrValue(typeName); + + if (!TextUtils.isEmpty(attrValue)) + { + attributeMap.put(attrTitle, attrValue); + } + } + } + } + public enum Category { NFT("NFT"), FT("Fungible Token"), COLLECTION("Collection"), SEMI_FT("Semi-Fungible"), ATTESTATION("Attestation"); diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index c49a391ca2..2c2cc53440 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -10,25 +10,34 @@ import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.repository.entity.RealmAttestation; +import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.AttestationValidationStatus; import com.alphawallet.token.entity.TokenScriptResult; import com.alphawallet.token.tools.Numeric; +import com.alphawallet.token.tools.TokenDefinition; +import com.google.gson.Gson; import org.json.JSONArray; import org.json.JSONObject; +import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; +import org.web3j.crypto.Sign; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import io.reactivex.Single; import timber.log.Timber; +import wallet.core.jni.Hash; /** * Created by JB on 23/02/2023. @@ -45,9 +54,12 @@ public class Attestation extends Token private final Map additionalMembers = new HashMap<>(); private boolean isValid; private ContractType baseTokenType = ContractType.ERC721; // default to ERC721 - private static final String VALID_FROM = "validFrom"; - private static final String VALID_TO = "validTo"; + private static final String VALID_FROM = "time"; + private static final String VALID_TO = "expirationTime"; private static final String TICKET_ID = "TicketId"; + private static final String SCRIPT_URI = "scriptURI"; + private static final String EVENT_ID = "eventId"; + private static final String SCHEMA_DATA_PREFIX = "data."; //TODO: Supplemental data @@ -82,9 +94,8 @@ public void handleValidation(AttestationValidation attValidation) addToMemberData(t.getKey(), t.getValue()); } - MemberData ticketId = new MemberData(TICKET_ID, attValidation._attestationId.longValue()); - ticketId.setIsSchema(); - additionalMembers.put(TICKET_ID, ticketId); + MemberData ticketId = new MemberData(SCHEMA_DATA_PREFIX + TICKET_ID, attValidation._attestationId.longValue()); + additionalMembers.put(SCHEMA_DATA_PREFIX + TICKET_ID, ticketId); } public void handleEASAttestation(EasAttestation attn, List names, List values, boolean isAttestationValid) @@ -92,7 +103,7 @@ public void handleEASAttestation(EasAttestation attn, List names, List type = values.get(index); addToMemberData(name, type); } @@ -104,8 +115,12 @@ public void handleEASAttestation(EasAttestation attn, List names, List validFrom && (validUntil == 0 || currentTime < validUntil); - additionalMembers.put(VALID_FROM, new MemberData(VALID_FROM, attn.time)); - additionalMembers.put(VALID_TO, new MemberData(VALID_TO, attn.expirationTime)); + + additionalMembers.put(VALID_FROM, new MemberData(VALID_FROM, attn.time).setIsTime()); + if (attn.expirationTime > 0) + { + additionalMembers.put(VALID_TO, new MemberData(VALID_TO, attn.expirationTime).setIsTime()); + } } public AttestationValidationStatus isValid() @@ -117,7 +132,7 @@ public AttestationValidationStatus isValid() } //Check attestation is being collected by the correct wallet (TODO: if not correct wallet, and wallet is present in user's wallets offer to switch wallet) - if (TextUtils.isEmpty(attestationSubject) || !attestationSubject.equalsIgnoreCase(getWallet())) + if (TextUtils.isEmpty(attestationSubject) || (!attestationSubject.equalsIgnoreCase(getWallet()) && !attestationSubject.equals("0"))) { return AttestationValidationStatus.Incorrect_Subject; } @@ -136,26 +151,92 @@ public String getAttestationUID() StringBuilder identifier = new StringBuilder(); for (Map.Entry m : additionalMembers.entrySet()) { - if (m.getValue().isSchemaValue()) + if (m.getValue().isURI() || !m.getValue().isSchemaValue() || m.getValue().isBytes()) //this may change between same attestations. + { + continue; + } + + if (identifier.length() > 0) + { + identifier.append("-"); + } + + identifier.append(m.getValue().getString()); + } + + return Numeric.toHexStringNoPrefix(Hash.keccak256(identifier.toString().getBytes(StandardCharsets.UTF_8))); + } + + public String getAttestationCollectionId() + { + String eventId = null; + MemberData eventMember = additionalMembers.get(EVENT_ID); + if (eventMember != null) + { + eventId = eventMember.getString(); + } + + EasAttestation easAttestation = getEasAttestation(); + + //issuer public key + //calculate hash from attestation + String hexStr = Numeric.cleanHexPrefix(easAttestation.schema) + "-" + Numeric.cleanHexPrefix(recoverPublicKey(easAttestation)) + (!TextUtils.isEmpty(eventId) ? ("-" + eventId) : ""); + //now convert this into ASCII hex bytes + byte[] collectionBytes = hexStr.getBytes(StandardCharsets.UTF_8); + //get Hash + byte[] hash = Hash.keccak256(collectionBytes); + + return Numeric.toHexString(hash); + } + + public String getAttestationDescription(TokenDefinition td) + { + TokenDefinition.Attestation att = td != null ? td.getAttestation() : null; + if (att != null && att.attributes != null && att.attributes.size() > 0) + { + return displayTokenScriptAttrs(att); + } + else + { + return displayIntrinsicAttrs(); + } + } + + private String displayTokenScriptAttrs(TokenDefinition.Attestation att) + { + StringBuilder identifier = new StringBuilder(); + for (Map.Entry attr : att.attributes.entrySet()) + { + String typeName = attr.getKey(); + String attrTitle = attr.getValue(); + MemberData attrVal = additionalMembers.get(typeName); + if (attrVal == null || !attrVal.isSchemaValue()) + { + continue; + } + + String attrValue = getAttrValue(typeName); + + if (!TextUtils.isEmpty(attrValue)) { if (identifier.length() > 0) { - identifier.append("-"); + identifier.append(" "); } - identifier.append(m.getValue().getString()); + identifier.append(attrTitle).append(": ").append(attrValue); } } return identifier.toString(); } - public String getAttestationDescription() + private String displayIntrinsicAttrs() { StringBuilder identifier = new StringBuilder(); for (Map.Entry m : additionalMembers.entrySet()) { - if (m.getValue().isSchemaValue()) + if (m.getValue().isSchemaValue() && !m.getValue().isURI() && !m.getValue().isBytes()) { if (identifier.length() > 0) { @@ -187,13 +268,20 @@ public void loadAttestationData(RealmAttestation rAtt) validUntil = validToData != null ? validToData.getValue().longValue() : 0; } + public boolean isEAS() + { + //Can we reconstruct EASAttestation? + EasAttestation easAttestation = getEasAttestation(); + return (easAttestation != null && easAttestation.getSignatureBytes().length == 65 && !TextUtils.isEmpty(easAttestation.version)); + } + @Override public void addAssetElements(NFTAsset asset, Context ctx) { //add all the attestation members for (Map.Entry m : additionalMembers.entrySet()) { - if (!m.getValue().isSchemaValue()) + if (!m.getValue().isSchemaValue() || m.getKey().contains(SCRIPT_URI)) { continue; } @@ -209,6 +297,21 @@ public void addAssetElements(NFTAsset asset, Context ctx) addDateToAttributes(asset, validTo, R.string.valid_until, ctx); } + @Override + public String getAttrValue(String typeName) + { + //pull out the value + MemberData attrVal = additionalMembers.get(typeName); + if (attrVal != null) + { + return attrVal.getString(); + } + else + { + return ""; + } + } + private void addDateToAttributes(NFTAsset asset, MemberData validFrom, int resource, Context ctx) { if (validFrom != null) @@ -230,9 +333,8 @@ private void patchLegacyAttestation(RealmAttestation rAtt) if (additionalMembers.isEmpty()) { BigInteger id = recoverId(rAtt); - MemberData tId = new MemberData(TICKET_ID, id.longValue()); - tId.setIsSchema(); - additionalMembers.put(TICKET_ID, tId); + MemberData tId = new MemberData(SCHEMA_DATA_PREFIX + TICKET_ID, id.longValue()); + additionalMembers.put(SCHEMA_DATA_PREFIX + TICKET_ID, tId); } } @@ -312,6 +414,35 @@ private void populateMembersFromJSON(String jsonData) } } + public EasAttestation getEasAttestation() + { + try + { + String rawAttestation = new String(attestation, StandardCharsets.UTF_8); + EasAttestation easAttn = new Gson().fromJson(rawAttestation, EasAttestation.class); + return easAttn; + } + catch (Exception e) //Expected + { + return null; + } + } + + public String getAttestationName(TokenDefinition td) + { + NFTAsset nftAsset = new NFTAsset(); + nftAsset.setupScriptElements(td); + String name = nftAsset.getName(); + if (!TextUtils.isEmpty(name)) + { + return name; + } + else + { + return tokenInfo.name; + } + } + private static class MemberData { JSONObject element; @@ -323,8 +454,15 @@ public MemberData(String name, Type type) element = new JSONObject(); element.put("name", name); element.put("type", type.getTypeAsString()); - element.put("value", type.getValue()); - element.put("isSchema", true); + if (type.getTypeAsString().equals("bytes")) + { + byte[] ddo = (byte[])type.getValue(); + element.put("value", Numeric.toHexString((byte[])type.getValue())); + } + else + { + element.put("value", type.getValue()); + } } catch (Exception e) { @@ -409,7 +547,15 @@ public String getString() { try { - return element.getString("value"); + String type = element.getString("type"); + if (type.equals("time")) + { + return formatDate(Long.parseLong(element.getString("value"))); + } + else + { + return element.getString("value"); + } } catch (Exception e) { @@ -433,11 +579,12 @@ public boolean isTrue() return false; } - public boolean isSchema() + public boolean isSchemaValue() { try { - return element.getBoolean("isSchema"); + String name = element.getString("name"); + return name.startsWith(SCHEMA_DATA_PREFIX); } catch (Exception e) { @@ -447,27 +594,47 @@ public boolean isSchema() return false; } - public void setIsSchema() + public boolean isURI() { try { - element.put("isSchema", true); + String name = element.getString("name"); + return name.endsWith(SCRIPT_URI); } catch (Exception e) { Timber.e(e); } + + return false; } - public boolean isSchemaValue() + public MemberData setIsTime() { - //true if this is integer or string try { - return element.getBoolean("isSchema") - && (element.getString("type").startsWith("uint") - || element.getString("type").startsWith("int") - || element.getString("type").equals("string")); + element.put("type", "time"); + } + catch (Exception e) + { + // + } + return this; + } + + private String formatDate(long time) + { + DateFormat f = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()); + String formattedDate = f.format(time*1000); + return formattedDate; + } + + public boolean isBytes() + { + try + { + String name = element.getString("type"); + return name.equals("bytes"); } catch (Exception e) { @@ -477,4 +644,76 @@ public boolean isSchemaValue() return false; } } + + /*@Override + public String getTSKey() + { + return tokenInfo.address.toLowerCase() + "-" + tokenInfo.chainId + "-" + getAttestationUID(); + }*/ + + @Override + public Single getScriptURI() + { + MemberData memberData = additionalMembers.get(SCHEMA_DATA_PREFIX + SCRIPT_URI); + if (memberData != null && !TextUtils.isEmpty(memberData.getString())) + { + return Single.fromCallable(memberData::getString); + } + else + { + return contractInteract.getScriptFileURI(); + } + } + + @Override + public Type getIntrinsicType(String name) + { + EasAttestation easAttestation; + switch (name) + { + case "attestation": + easAttestation = getEasAttestation(); + if (easAttestation != null) + { + return easAttestation.getAttestationCore(); + } + break; + case "attestationSig": + easAttestation = getEasAttestation(); + if (easAttestation != null) + { + return new DynamicBytes(easAttestation.getSignatureBytes()); + } + break; + default: + break; + } + + return null; + } + + private static String recoverPublicKey(EasAttestation attestation) + { + String recoveredKey = ""; + + try + { + StructuredDataEncoder dataEncoder = new StructuredDataEncoder(attestation.getEIP712Attestation()); + byte[] hash = dataEncoder.hashStructuredData(); + byte[] r = Numeric.hexStringToByteArray(attestation.getR()); + byte[] s = Numeric.hexStringToByteArray(attestation.getS()); + byte v = (byte)(attestation.getV() & 0xFF); + + Sign.SignatureData sig = new Sign.SignatureData(v, r, s); + + BigInteger key = Sign.signedMessageHashToKey(hash, sig); + recoveredKey = Numeric.toHexString(Numeric.toBytesPadded(key, 64)); + } + catch (Exception e) + { + e.printStackTrace(); + } + + return recoveredKey; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index 312fa7e800..25bde8c07e 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -35,9 +35,11 @@ import com.alphawallet.token.entity.ContractAddress; import com.alphawallet.token.entity.TicketRange; import com.alphawallet.token.entity.TokenScriptResult; +import com.alphawallet.token.tools.TokenDefinition; import org.web3j.abi.datatypes.Event; import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; import org.web3j.protocol.core.DefaultBlockParameter; import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.EthLog; @@ -657,10 +659,15 @@ public boolean hasArrayBalance() public String getTokenName(AssetDefinitionService assetService, int count) { + String name = ""; //see if this token is covered by any contract - if (assetService != null && assetService.hasDefinition(tokenInfo.chainId, tokenInfo.address)) + if (assetService != null && assetService.hasDefinition(this)) { - String name = assetService.getAssetDefinition(tokenInfo.chainId, getAddress()).getTokenName(count); + TokenDefinition td = assetService.getAssetDefinition(tokenInfo.chainId, getAddress()); + if (td != null) + { + name = td.getTokenName(count); + } if (isNonFungible() && !TextUtils.isEmpty(name)) { return name; @@ -669,15 +676,13 @@ else if (!TextUtils.isEmpty(tokenInfo.name)) { return tokenInfo.name; } - else + else if (!TextUtils.isEmpty(name)) { return name; } } - else - { - return tokenInfo.name; - } + + return tokenInfo.name; } public boolean hasRealValue() @@ -1070,7 +1075,6 @@ public Single getScriptURI() return contractInteract.getScriptFileURI(); } - /** * Event filters for send and receive of the token, overriden by the token type */ @@ -1093,4 +1097,26 @@ public void addAssetElements(NFTAsset asset, Context ctx) { } + + public String getTSKey() + { + if (isEthereum()) + { + return "ethereum-" + tokenInfo.chainId; + } + else + { + return tokenInfo.address.toLowerCase() + "-" + tokenInfo.chainId; + } + } + + public Type getIntrinsicType(String name) + { + return null; + } + + public String getAttrValue(String typeName) + { + return ""; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java index b10a4608d1..7242d11f31 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java @@ -53,7 +53,7 @@ public TokenCardMeta(long chainId, String tokenAddress, String balance, long tim this.lastUpdate = timeStamp; this.type = type; this.balance = balance; - this.nameWeight = calculateTokenNameWeight(chainId, tokenAddress, svs, name, symbol, isEthereum(), group, attnId.hashCode()); + this.nameWeight = calculateTokenNameWeight(chainId, tokenAddress, svs, name, symbol, isEthereum(), group, Math.abs(attnId.hashCode())); this.filterText = symbol + "'" + name; this.group = group; } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java index a547c89b28..50deaba959 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java @@ -418,6 +418,17 @@ public Function generateTransactionFunction(Token token, BigInteger tokenId, Tok if (value == null) throw new Exception("Attempt to use null value"); params.add(new DynamicBytes(Numeric.hexStringToByteArray(value))); break; + case "struct": + Type intrinsicType = token.getIntrinsicType(arg.element.ref); + if (intrinsicType == null) + { + intrinsicType = token.getIntrinsicType(arg.element.localRef); + } + if (intrinsicType != null) + { + params.add(intrinsicType); + } + break; case "bytes1": params.add(new Bytes1(argValueBytes)); break; diff --git a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java index 422634c1d7..cf04d6d135 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java @@ -1,5 +1,6 @@ package com.alphawallet.app.repository; +import static com.alphawallet.app.service.AssetDefinitionService.getEASContract; import static com.alphawallet.app.service.TickerService.TICKER_TIMEOUT; import static com.alphawallet.app.service.TokensService.EXPIRED_CONTRACT; @@ -310,11 +311,14 @@ public Token fetchToken(long chainId, Wallet wallet, String address) private Token fetchAttestation(long chainId, Wallet wallet, RealmAttestation rAttn) { - Token token = fetchToken(chainId, wallet, rAttn.getTokenAddress()); - TokenInfo tInfo = token != null ? token.tokenInfo : Utils.getDefaultAttestationInfo(chainId); + Token token = fetchToken(chainId, wallet, rAttn.getTokenAddress()); //<-- getTokenAddress() should be the key + //We require to + rAttn.getAttestation(); + TokenInfo tInfo = token != null ? token.tokenInfo : Utils.getDefaultAttestationInfo(chainId, rAttn.getTokenAddress()); Attestation att = new Attestation(tInfo, ethereumNetworkRepository.getNetworkByChain(chainId).getShortName(), rAttn.getAttestation()); - att.setTokenWallet(wallet.address); att.loadAttestationData(rAttn); + att.setTokenWallet(wallet.address); + return att; } diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index 8989ea5aa3..0bf7a87cfb 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -2,7 +2,6 @@ import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; import static com.alphawallet.app.repository.TokensRealmSource.IMAGES_DB; -import static com.alphawallet.app.repository.TokensRealmSource.databaseKey; import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_ID; @@ -22,6 +21,7 @@ import android.util.Pair; import androidx.annotation.Keep; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.lifecycle.MutableLiveData; @@ -33,6 +33,7 @@ import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.QueryResponse; import com.alphawallet.app.entity.TokenLocator; +import com.alphawallet.app.entity.attestation.SchemaRecord; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Attestation; import com.alphawallet.app.entity.tokens.Token; @@ -48,12 +49,10 @@ import com.alphawallet.app.ui.HomeActivity; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.HomeViewModel; -import com.alphawallet.app.web3j.StructuredData; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.ethereum.NetworkInfo; import com.alphawallet.token.entity.ActionModifier; -import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.AttributeInterface; import com.alphawallet.token.entity.ContractAddress; @@ -76,20 +75,20 @@ import com.alphawallet.token.tools.TokenDefinition; import org.jetbrains.annotations.NotNull; +import org.web3j.abi.DefaultFunctionEncoder; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Bool; -import org.web3j.abi.datatypes.Bytes; -import org.web3j.abi.datatypes.DynamicArray; -import org.web3j.abi.datatypes.DynamicStruct; +import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Function; -import org.web3j.abi.datatypes.StaticStruct; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.abi.datatypes.generated.Uint64; +import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; @@ -140,6 +139,7 @@ import io.realm.exceptions.RealmException; import io.realm.exceptions.RealmPrimaryKeyConstraintException; import timber.log.Timber; +import wallet.core.jni.Hash; /** @@ -428,7 +428,8 @@ private TokenDefinition fileLoadComplete(List originContracts, realm.executeTransaction(r -> { for (ContractLocator cl : originContracts) { - String entryKey = getTSDataKey(cl.chainId, cl.address); + //String entryKey = getTSDataKey(cl.chainId, cl.address); + String entryKey = getTSDataKeyTemp(cl.chainId, cl.address); RealmTokenScriptData entry = r.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) .findFirst(); @@ -459,9 +460,14 @@ private TokenDefinition fileLoadComplete(List originContracts, return td; } - private String getTSDataKey(long chainId, String address) + private String getTSDataKeyTemp(long chainId, String address) { - return address + "-" + chainId; + if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) + { + address = "ethereum"; + } + + return address.toLowerCase() + "-" + chainId; } //Start listening to the two script directories for files dropped in. @@ -835,10 +841,17 @@ private boolean checkReadPermission() == PackageManager.PERMISSION_GRANTED; } - private TokenDefinition getDefinition(long chainId, String address) + private TokenDefinition getDefinition(String tsKey) { - if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) address = "ethereum"; TokenDefinition result = null; + String[] elements = tsKey.split("-"); + if (elements.length < 2) + { + return null; + } + + String address = elements[0]; + long chainId = Long.parseLong(elements[1]); //try cache if (cachedDefinition != null) { @@ -856,7 +869,7 @@ private TokenDefinition getDefinition(long chainId, String address) try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", tsKey) .findFirst(); if (tsData != null) @@ -884,11 +897,27 @@ private TokenDefinition getDefinition(long chainId, String address) public TokenScriptFile getTokenScriptFile(long chainId, String address) { //pull from database - if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) address = "ethereum"; try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", getTSDataKeyTemp(chainId, address))// getTSDataKey(chainId, address)) + .findFirst(); + + if (tsData != null && tsData.getFilePath() != null) + { + return new TokenScriptFile(context, tsData.getFilePath()); + } + } + + return new TokenScriptFile(context); + } + + public TokenScriptFile getTokenScriptFile(Token token) + { + try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) + { + RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) + .equalTo("instanceKey", token.getTSKey()) .findFirst(); if (tsData != null && tsData.getFilePath() != null) @@ -915,7 +944,7 @@ public TokenDefinition getAssetDefinition(long chainId, String address) address = "ethereum"; } //is asset definition currently read? - final TokenDefinition assetDef = getDefinition(chainId, address.toLowerCase()); + final TokenDefinition assetDef = getDefinition(getTSDataKeyTemp(chainId, address)); if (assetDef == null && !address.equals("ethereum")) { //try web @@ -925,18 +954,23 @@ public TokenDefinition getAssetDefinition(long chainId, String address) return assetDef; // if nothing found use default } - public Single getAssetDefinitionASync(long chainId, final String address) + public Single getAssetDefinitionASync(long chainId, String address) { - if (address == null) return Single.fromCallable(TokenDefinition::new); - String contractName = address; - if (contractName.equalsIgnoreCase(tokensService.getCurrentAddress())) - contractName = "ethereum"; + if (address == null) + { + return Single.fromCallable(TokenDefinition::new); + } - // hold until asset definitions have finished loading - waitForAssets(); + String convertedAddr = (address.equalsIgnoreCase(tokensService.getCurrentAddress())) ? "ethereum" : address.toLowerCase(); + return getAssetDefinitionASync(getDefinition(getTSDataKeyTemp(chainId, address)), convertedAddr); + } - final TokenDefinition assetDef = getDefinition(chainId, contractName.toLowerCase()); - if (assetDef != null) return Single.fromCallable(() -> assetDef); + private Single getAssetDefinitionASync(final TokenDefinition assetDef, final String contractName) + { + if (assetDef != null) + { + return Single.fromCallable(() -> assetDef); + } else if (!contractName.equals("ethereum")) { //at this stage, this script isn't replacing any existing script, so it's safe to write to database without checking if we need to delete anything @@ -946,6 +980,20 @@ else if (!contractName.equals("ethereum")) else return Single.fromCallable(TokenDefinition::new); } + public Single getAssetDefinitionASync(Token token) + { + String contractName = token.tokenInfo.address; + if (contractName.equalsIgnoreCase(tokensService.getCurrentAddress())) + { + contractName = "ethereum"; + } + + // hold until asset definitions have finished loading + waitForAssets(); + + return getAssetDefinitionASync(getDefinition(token.getTSKey()), contractName); + } + private void waitForAssets() { try @@ -969,7 +1017,7 @@ public String getTokenName(long chainId, String address, int count) try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", getTSDataKeyTemp(chainId, address)) .findFirst(); if (tsData != null) @@ -995,15 +1043,12 @@ public Token getTokenFromService(long chainId, String address) */ public String getIssuerName(Token token) { - long chainId = token.tokenInfo.chainId; - String address = token.tokenInfo.address; - String issuer = token.getNetworkName(); try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", token.getTSKey()) .findFirst(); if (tsData != null) @@ -1107,7 +1152,7 @@ private void updateScriptEntriesInRealm(List origins, boolean i realm.executeTransaction(r -> { for (ContractLocator cl : origins) { - String entryKey = getTSDataKey(cl.chainId, cl.address); + String entryKey = getTSDataKeyTemp(cl.chainId, cl.address); RealmTokenScriptData realmData = r.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) .findFirst(); @@ -1137,7 +1182,7 @@ private Single fetchTokenScriptFromContract(Token token, MutableLiveData { - if (!TextUtils.isEmpty(uri)) + if (!TextUtils.isEmpty(uri) && updateFlag != null) { updateFlag.postValue(true); } @@ -1156,7 +1201,9 @@ private File storeEntry(Token token, Pair> scriptD return new File(UNCHANGED_SCRIPT); // blank file with UNCHANGED name } - File storeFile = storeFile(token.tokenInfo.address, scriptData.second); + String tokenKey = token.getTSKey(); + + File storeFile = storeFile(tokenKey, scriptData.second); TokenScriptFile tsf = new TokenScriptFile(context, storeFile.getAbsolutePath()); @@ -1165,14 +1212,14 @@ private File storeEntry(Token token, Pair> scriptD try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { realm.executeTransaction(r -> { - String entryKey = getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); + //String entryKey = getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); RealmTokenScriptData entry = r.where(RealmTokenScriptData.class) - .equalTo("instanceKey", entryKey) + .equalTo("instanceKey", tokenKey) .findFirst(); if (entry == null) { - entry = r.createObject(RealmTokenScriptData.class, entryKey); + entry = r.createObject(RealmTokenScriptData.class, tokenKey); } entry.setFileHash(fileHash); @@ -1194,15 +1241,7 @@ private String compareExistingScript(Token token, String uri) String returnUri = uri; try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { - String entryKey = getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); - - /*realm.executeTransaction(r -> { - RealmTokenScriptData entry = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", entryKey) - .findFirst(); - - entry.setIpfsPath(""); - });*/ + String entryKey = token.getTSKey(); //getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); RealmTokenScriptData entry = realm.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) @@ -1427,7 +1466,7 @@ private void updateRealmForBundledScript(long chainId, String address, String as try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { realm.executeTransaction(r -> { - String entryKey = getTSDataKey(chainId, address); + String entryKey = getTSDataKeyTemp(chainId, address); RealmTokenScriptData entry = r.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) .findFirst(); @@ -1947,14 +1986,13 @@ private File storeFile(String address, Pair result) throws IOEx return file; } - public boolean hasDefinition(long chainId, String address) + public boolean hasDefinition(Token token) { boolean hasDefinition = false; - if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) address = "ethereum"; try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", token.getTSKey()) .findFirst(); hasDefinition = tsData != null; @@ -1969,13 +2007,12 @@ public void clearCheckTimes() assetChecked.clear(); } - public boolean hasTokenView(long chainId, String address, String type) + public boolean hasTokenView(Token token, String type) { - if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) address = "ethereum"; try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", getTSDataKey(chainId, address)) + .equalTo("instanceKey", token.getTSKey()) .findFirst(); return (tsData != null && tsData.getViewList().size() > 0); @@ -2311,14 +2348,25 @@ private void notifyNewScript(TokenDefinition tokenDefinition, File file) return observer; } + public Single getSignatureData(Token token) + { + TokenScriptFile tsf = getTokenScriptFile(token); + return getSignatureData(tsf); + } + public Single getSignatureData(long chainId, String contractAddress) + { + TokenScriptFile tsf = getTokenScriptFile(chainId, contractAddress); + return getSignatureData(tsf); + } + + private Single getSignatureData(TokenScriptFile tsf) { return Single.fromCallable(() -> { XMLDsigDescriptor sigDescriptor = new XMLDsigDescriptor(); sigDescriptor.result = "fail"; sigDescriptor.type = SigReturnType.NO_TOKENSCRIPT; - TokenScriptFile tsf = getTokenScriptFile(chainId, contractAddress); if (tsf != null && tsf.exists()) { String hash = tsf.calcMD5(); @@ -2608,23 +2656,73 @@ public Observable resolveAttrs(Token token, TokenDe return resolveAttrs(token, tokenId, definition, attrList, itemView); } - public TokenScriptResult.Attribute getAvailableAttestation(Token token, TSAction action, String attnId) + /*private Function formValidation(EasAttestation attn) + { + //Commented out + byte[] signature = attn.getSignatureBytes(); + + List inputParams = Arrays.asList(attn.getAttestationCore(), new DynamicBytes(signature)); + DefaultFunctionEncoder dfe = new DefaultFunctionEncoder(); + + List inputParam = Collections.singletonList(attn.getAttestationCore()); + String hex = dfe.encodeParameters(inputParam); + //trim struct definition + hex = hex.substring(0x40); + + List> returnTypes = Arrays.asList(new TypeReference() { }, new TypeReference
() { }, new TypeReference
() { }, new TypeReference() { }, new TypeReference() { }); + Function testValidation = new Function("verifyEASAttestation", + inputParams, + returnTypes); + + String contractAddress = "0xeAC4F618232B5cA1C895B6e5468363fdd128E873"; + + String result = tokenscriptUtility.callSmartContract(attn.getChainId(), contractAddress, testValidation); + List values = FunctionReturnDecoder.decode(result, testValidation.getOutputParameters()); + + inputParams.clear(); + //(string memory eventId, string memory ticketId, uint8 ticketClass, bytes memory commitment) + returnTypes = Arrays.asList(new TypeReference() { }, new TypeReference() { }, new TypeReference() { }, new TypeReference() { }); + + + //OK. Try decode + testValidation = new Function("decodeAttestationData", + inputParam, + returnTypes); + + + return testValidation; + }*/ + + public List getAttestationAttrs(Token token, TSAction action, String attnId) { + List attrs = new ArrayList<>(); Attestation att = (action != null && action.modifier == ActionModifier.ATTESTATION) ? (Attestation) tokensService.getAttestation(token.tokenInfo.chainId, token.getAddress(), attnId) : null; if (att != null) { - return new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, Numeric.toHexString(att.getAttestation())); - } - else - { - return null; + if (att.isEAS()) + { + //can we rebuild the EasAttestation? + DefaultFunctionEncoder dfe = new DefaultFunctionEncoder(); + EasAttestation easAttestation = att.getEasAttestation(); + List inputParam = Collections.singletonList(easAttestation.getAttestationCore()); + String coreAttestationHex = "0x" + dfe.encodeParameters(inputParam).substring(0x40); + attrs.add(new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, coreAttestationHex)); + //also require signature + String signatureBytes = Numeric.toHexString(easAttestation.getSignatureBytes()); + attrs.add(new TokenScriptResult.Attribute("attestationSig", "attestationSig", BigInteger.ZERO, signatureBytes)); + } + else + { + attrs.add(new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, Numeric.toHexString(att.getAttestation()))); + } } + + return attrs; } - public Map getAttestationFunctionMap(long chainId, String address, String attnId) + public Map getAttestationFunctionMap(Token att) { - Token att = tokensService.getAttestation(chainId, address, attnId); - TokenDefinition td = getAssetDefinition(chainId, address); + TokenDefinition td = getAssetDefinition(att.tokenInfo.chainId, att.tokenInfo.address); Map actions = new HashMap<>(); if (att != null && td != null) { @@ -2819,7 +2917,8 @@ public Single> getAllTokenDefinitions(boolean refresh) public Single checkServerForScript(Token token, MutableLiveData updateFlag) { - TokenScriptFile tf = getTokenScriptFile(token.tokenInfo.chainId, token.getAddress()); + TokenScriptFile tf = getTokenScriptFile(token); + if ((tf != null && !TextUtils.isEmpty(tf.getName())) && !isInSecureZone(tf)) { return Single.fromCallable(TokenDefinition::new); //early return for debug script check @@ -2975,7 +3074,7 @@ private void deleteAWRealm() public Attestation validateAttestation(String attestation, TokenInfo tInfo) { - TokenDefinition td = getDefinition(tInfo.chainId, tInfo.address); + TokenDefinition td = getDefinition(getTSDataKeyTemp(tInfo.chainId, tInfo.address));//getDefinition(tInfo.chainId, tInfo.address); Attestation att = null; if (td != null) @@ -3029,16 +3128,35 @@ public Attestation validateAttestation(EasAttestation attestation, String import List values = decodeAttestationData(attestation.data, attestationSchema.schema, names); NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(attestation.getChainId()); - //For EAS attestation use the EAS contract as the token base - TokenInfo tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId()); + + TokenInfo tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId(), getEASContract(attestation.chainId)); Attestation localAttestation = new Attestation(tInfo, networkInfo.name, importedAttestation.getBytes(StandardCharsets.UTF_8)); + localAttestation.handleEASAttestation(attestation, names, values, attestationValid); + + String collectionHash = localAttestation.getAttestationCollectionId(); + //Now regenerate with the correct collectionId + tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId(), collectionHash); + localAttestation = new Attestation(tInfo, networkInfo.name, importedAttestation.getBytes(StandardCharsets.UTF_8)); localAttestation.handleEASAttestation(attestation, names, values, attestationValid); localAttestation.setTokenWallet(tokensService.getCurrentAddress()); return localAttestation; } - private List decodeAttestationData(String attestationData, String decodeSchema, List names) + private String getDataValue(String key, List names, List values) + { + Map valueMap = new HashMap<>(); + for (int index = 0; index < names.size(); index++) + { + String name = names.get(index); + Type type = values.get(index); + valueMap.put(name, type.toString()); + } + + return valueMap.get(key); + } + + private List decodeAttestationData(String attestationData, @NonNull String decodeSchema, List names) { List> returnTypes = new ArrayList>(); //build decoder @@ -3074,7 +3192,7 @@ else if (type.startsWith("bytes") && !type.equals("bytes")) tRef = new TypeReference() { }; break; case "bytes": - tRef = new TypeReference() { }; + tRef = new TypeReference() { }; break; case "bool": tRef = new TypeReference() { }; @@ -3100,34 +3218,6 @@ else if (type.startsWith("bytes") && !type.equals("bytes")) return FunctionReturnDecoder.decode(attestationData, org.web3j.abi.Utils.convert(returnTypes)); } - public static class SchemaRecord extends DynamicStruct - { - public byte[] uid; - public Address resolver; - public boolean revocable; - public String schema; - - public SchemaRecord(byte[] uid, Address resolver, boolean revocable, String schema) { - super( - new org.web3j.abi.datatypes.generated.Bytes32(uid), - new org.web3j.abi.datatypes.Address(resolver.getValue()), - new org.web3j.abi.datatypes.Bool(revocable), - new org.web3j.abi.datatypes.Utf8String(schema)); - this.uid = uid; - this.resolver = resolver; - this.revocable = revocable; - this.schema = schema; - } - - public SchemaRecord(Bytes32 uid, Address resolver, Bool revocable, Utf8String schema) { - super(uid, resolver, revocable, schema); - this.uid = uid.getValue(); - this.resolver = resolver; - this.revocable = revocable.getValue(); - this.schema = schema.getValue(); - } - } - private SchemaRecord fetchSchemaRecord(long chainId, String schemaUID) { //1. Resolve UID. For now, just use default: This should be on a switch for chains diff --git a/app/src/main/java/com/alphawallet/app/service/GasService.java b/app/src/main/java/com/alphawallet/app/service/GasService.java index a7597f0600..f448b1c4fd 100644 --- a/app/src/main/java/com/alphawallet/app/service/GasService.java +++ b/app/src/main/java/com/alphawallet/app/service/GasService.java @@ -318,6 +318,21 @@ private boolean updateEIP1559Realm(final Map re } public Single calculateGasEstimate(byte[] transactionBytes, long chainId, String toAddress, + BigInteger amount, Wallet wallet, final BigInteger defaultLimit) + { + updateChainId(chainId); + if (currentGasPrice == null) + { + return useNodeEstimate() + .flatMap(com -> calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit)); + } + else + { + return calculateGasEstimateInternal(transactionBytes, chainId, toAddress, amount, wallet, defaultLimit); + } + } + + public Single calculateGasEstimateInternal(byte[] transactionBytes, long chainId, String toAddress, BigInteger amount, Wallet wallet, final BigInteger defaultLimit) { String txData = ""; diff --git a/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java b/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java index 1eb02a6d95..fe93481fa6 100644 --- a/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java +++ b/app/src/main/java/com/alphawallet/app/service/PriceAlertsService.java @@ -187,7 +187,7 @@ private void updatePriceAlerts(List priceAlerts) private Intent constructIntent(Token token) { - boolean hasDefinition = assetDefinitionService.hasDefinition(token.tokenInfo.chainId, token.getAddress()); + boolean hasDefinition = assetDefinitionService.hasDefinition(token); return tokenDetailRouter.makeERC20DetailsIntent(this, token.getAddress(), token.tokenInfo.symbol, token.tokenInfo.decimals, !token.isEthereum(), defaultWallet, token, hasDefinition); } @@ -205,4 +205,4 @@ private String getIndicatorText(boolean isAbove) } return getString(R.string.price_alert_indicator_below); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java index dc26044868..bf1284ba5b 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java @@ -156,7 +156,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) viewModel.checkTokenScriptValidity(token); token.clearResultMap(); - if (token.getArrayBalance().size() > 0 && viewModel.getAssetDefinitionService().hasDefinition(token.tokenInfo.chainId, token.tokenInfo.address)) + if (token.getArrayBalance().size() > 0 && viewModel.getAssetDefinitionService().hasDefinition(token)) { loadItemViewHeight(); } @@ -195,7 +195,7 @@ private void viewHeight(int fetchedViewHeight) private void onNewScript(Boolean aBoolean) { //need to reload tokens, now we have an updated/new script - if (viewModel.getAssetDefinitionService().hasDefinition(token.tokenInfo.chainId, token.tokenInfo.address)) + if (viewModel.getAssetDefinitionService().hasDefinition(token)) { initWebViewCheck(); handler.postDelayed(this, TOKEN_SIZING_DELAY); @@ -361,7 +361,7 @@ public void handleTokenScriptFunction(String function, List selectio } else { - viewModel.showFunction(this, token, function, selection); + viewModel.showFunction(this, token, function, selection, null); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java index 26c1ef9100..caa3736ee3 100644 --- a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java @@ -32,6 +32,7 @@ import com.alphawallet.app.entity.TransactionReturn; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokenscript.TokenScriptRenderCallback; import com.alphawallet.app.entity.tokenscript.WebCompletionCallback; @@ -91,9 +92,9 @@ public class FunctionActivity extends BaseActivity implements FunctionCallback, WebCompletionCallback, OnSetValuesListener, ActionSheetCallback { private TokenFunctionViewModel viewModel; - private Token token; private List tokenIds; + private NFTAsset asset; private BigInteger tokenId; private String actionMethod; private Web3TokenView tokenView; @@ -111,8 +112,8 @@ private void initViews() { String tokenIdStr = getIntent().getStringExtra(C.EXTRA_TOKEN_ID); if (tokenIdStr == null || tokenIdStr.length() == 0) tokenIdStr = "0"; - String address = getIntent().getStringExtra(C.EXTRA_ADDRESS); Wallet wallet = getIntent().getParcelableExtra(C.Key.WALLET); + asset = getIntent().getParcelableExtra(C.EXTRA_NFTASSET); if (wallet == null) { viewModel.getCurrentWallet(); } else { @@ -120,7 +121,8 @@ private void initViews() { } long chainId = getIntent().getLongExtra(C.EXTRA_CHAIN_ID, EthereumNetworkBase.MAINNET_ID); - token = viewModel.getTokenService().getToken(wallet.address, chainId, address); + token = resolveAssetToken(wallet, chainId); + //token = viewModel.getTokenService().getToken(wallet.address, chainId, address); if (token == null) { @@ -143,6 +145,18 @@ private void initViews() { parsePass = 0; } + private Token resolveAssetToken(Wallet wallet, long chainId) + { + if (asset != null && asset.isAttestation()) + { + return viewModel.getTokenService().getAttestation(chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS), asset.getAttestationID()); + } + else + { + return viewModel.getTokensService().getToken(wallet.address, chainId, getIntent().getStringExtra(C.EXTRA_ADDRESS)); + } + } + private void displayFunction(String tokenAttrs) { try @@ -188,11 +202,8 @@ private void getAttrs() action = functions.get(actionMethod); List localAttrs = (action != null && action.attributes != null) ? new ArrayList<>(action.attributes.values()) : null; - TokenScriptResult.Attribute attestation = viewModel.getAssetDefinitionService().getAvailableAttestation(token, action, getIntent().getStringExtra(C.EXTRA_ATTESTATION_ID)); - if (attestation != null) - { - onAttr(attestation); - } + //Add attestation attributes + addAttestationAttrs(); viewModel.getAssetDefinitionService().resolveAttrs(token, tokenIds, localAttrs) .subscribeOn(Schedulers.io()) @@ -684,6 +695,21 @@ public void testRecoverAddressFromSignature(String message, String sig) } } + private void addAttestationAttrs() + { + if (asset != null && asset.isAttestation()) + { + List attestationAttrs = viewModel.getAssetDefinitionService().getAttestationAttrs(token, action, asset.getAttestationID()); + if (attestationAttrs != null) + { + for (TokenScriptResult.Attribute attr : attestationAttrs) + { + onAttr(attr); + } + } + } + } + private void onProgress(boolean shouldShowProgress) { hideDialog(); if (shouldShowProgress) { diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java index 60f267497e..648e6ed176 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTActivity.java @@ -112,7 +112,7 @@ private void syncListener() private boolean hasTokenScriptOverride(Token t) { - return viewModel.getAssetDefinitionService().hasTokenView(t.tokenInfo.chainId, t.getAddress(), AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME); + return viewModel.getAssetDefinitionService().hasTokenView(t, AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME); } private void onSignature(XMLDsigDescriptor descriptor) diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index db1bd793ba..f57a731c4c 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.text.Html; import android.text.TextUtils; +import android.util.Pair; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -28,6 +29,7 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.GasEstimate; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionReturn; @@ -53,8 +55,10 @@ import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.hardware.SignatureFromKey; import com.alphawallet.token.entity.TSAction; +import com.alphawallet.token.entity.TokenScriptResult; import com.alphawallet.token.entity.TokenScriptResult.Attribute; import com.alphawallet.token.entity.XMLDsigDescriptor; +import com.alphawallet.token.tools.TokenDefinition; import java.math.BigDecimal; import java.math.BigInteger; @@ -250,6 +254,8 @@ private void getIntentData() token = resolveAssetToken(); setup(); } + + viewModel.startGasPriceUpdate(chainId); } private Token resolveAssetToken() @@ -322,6 +328,7 @@ private void initViewModel() viewModel = new ViewModelProvider(this) .get(TokenFunctionViewModel.class); viewModel.gasEstimateComplete().observe(this, this::checkConfirm); + viewModel.gasEstimateError().observe(this, this::estimateError); viewModel.nftAsset().observe(this, this::onNftAsset); viewModel.transactionFinalised().observe(this, this::txWritten); viewModel.transactionError().observe(this, this::txError); @@ -333,10 +340,10 @@ private void initViewModel() private void newScriptFound(Boolean status) { + CertifiedToolbarView certificateToolbar = findViewById(R.id.certified_toolbar); //determinate signature if (token != null && status) { - CertifiedToolbarView certificateToolbar = findViewById(R.id.certified_toolbar); certificateToolbar.stopDownload(); certificateToolbar.setVisibility(View.VISIBLE); viewModel.checkTokenScriptValidity(token); @@ -345,6 +352,13 @@ private void newScriptFound(Boolean status) //now re-load the verbs setupFunctionBar(viewModel.getWallet()); + + setupAttestation(); + } + else + { + certificateToolbar.stopDownload(); + //setupAttestation(); } } @@ -388,6 +402,32 @@ private void setupFunctionBar(Wallet wallet) } } + private void completeAttestationTokenScriptSetup(TSAction action) + { + List attestationAttrs = viewModel.getAssetDefinitionService().getAttestationAttrs(token, action, asset.getAttestationID()); + if (attestationAttrs != null) + { + for (TokenScriptResult.Attribute attr : attestationAttrs) + { + token.setAttributeResult(BigInteger.ONE, attr); + } + } + } + + private void completeTokenScriptSetup() + { + final List attrs = new ArrayList<>(); + + if (viewModel.hasTokenScript(token)) + { + viewModel.getAssetDefinitionService().resolveAttrs(token, new ArrayList<>(Collections.singleton(tokenId)), null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(attrs::add, this::onError, () -> showTSAttributes(attrs)) + .isDisposed(); + } + } + private void reloadMetadata() { triggeredReload = true; @@ -436,6 +476,9 @@ private void updateDefaultTokenData() case ERC1155: tivTokenStandard.setValue(getString(R.string.erc1155)); break; + case ATTESTATION: + tivContractAddress.setVisibility(View.GONE); + break; case ERC721_UNDETERMINED: default: break; @@ -456,16 +499,7 @@ private void loadAssetFromMetadata(NFTAsset loadedAsset) loadFromOpenSeaData(loadedAsset.getOpenSeaAsset()); - final List attrs = new ArrayList<>(); - - if (viewModel.hasTokenScript(token)) - { - viewModel.getAssetDefinitionService().resolveAttrs(token, new ArrayList<>(Collections.singleton(tokenId)), null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(attrs::add, this::onError, () -> showTSAttributes(attrs)) - .isDisposed(); - } + completeTokenScriptSetup(); } } @@ -591,12 +625,29 @@ private void onOpenSeaAsset(OpenSeaAsset openSeaAsset) private void setupAttestation() { - tokenImage.setImageResource(R.drawable.zero_one_block); - progressBar.setVisibility(View.GONE); NFTAsset attnAsset = new NFTAsset(); - token.addAssetElements(attnAsset, this); + TokenDefinition td = viewModel.getAssetDefinitionService().getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + if (td != null) + { + attnAsset.setupScriptElements(td); + attnAsset.setupScriptAttributes(td, token); + tokenImage.setupTokenImage(attnAsset); + setTitle(attnAsset.getName()); + if (!TextUtils.isEmpty(attnAsset.getDescription())) + { + tokenDescription.setVisibility(View.VISIBLE); + tokenDescription.setText(attnAsset.getDescription()); + } + } + else + { + tokenImage.setImageResource(R.drawable.zero_one_block); + token.addAssetElements(attnAsset, this); + tokenDescription.setVisibility(View.GONE); + } + + progressBar.setVisibility(View.GONE); tivTokenId.setVisibility(View.GONE); - tokenDescription.setVisibility(View.GONE); //now populate nftAttributeLayout.bind(token, attnAsset); @@ -658,11 +709,17 @@ public void handleTokenScriptFunction(String function, List selectio TSAction action = functions.get(function); token.clearResultMap(); + BigInteger tokenId = selection.size() > 0 ? selection.get(0) : BigInteger.ONE; + //handle TS function if (action != null && action.view == null && action.function != null) { + //test if we need to build attribute the list + completeAttestationTokenScriptSetup(action); + //viewModel.loadAttributesIfRequired(); + //open action sheet after we determine the gas limit - Web3Transaction web3Tx = viewModel.handleFunction(action, selection.get(0), token, this); + Web3Transaction web3Tx = viewModel.handleFunction(action, tokenId, token, this); if (web3Tx.gasLimit.equals(BigInteger.ZERO)) { calculateEstimateDialog(); @@ -677,7 +734,7 @@ public void handleTokenScriptFunction(String function, List selectio } else { - viewModel.showFunction(this, token, function, selection); + viewModel.showFunction(this, token, function, selection, asset); } } @@ -692,44 +749,39 @@ private void calculateEstimateDialog() dialog.show(); } - private void estimateError(final Web3Transaction w3tx) + private void estimateError(Pair estimate) { + if (dialog != null && dialog.isShowing()) dialog.dismiss(); if (dialog != null && dialog.isShowing()) dialog.dismiss(); dialog = new AWalletAlertDialog(this); dialog.setIcon(WARNING); - dialog.setTitle(R.string.confirm_transaction); - dialog.setMessage(R.string.error_transaction_may_fail); - dialog.setButtonText(R.string.button_ok); + dialog.setTitle(estimate.first.hasError() ? + R.string.dialog_title_gas_estimation_failed : + R.string.confirm_transaction + ); + String message = estimate.first.hasError() ? + getString(R.string.dialog_message_gas_estimation_failed, estimate.first.getError()) : + getString(R.string.error_transaction_may_fail); + dialog.setMessage(message); + dialog.setButtonText(R.string.action_proceed); dialog.setSecondaryButtonText(R.string.action_cancel); - dialog.setButtonListener(v -> - { + dialog.setButtonListener(v -> { + Web3Transaction w3tx = estimate.second; BigInteger gasEstimate = GasService.getDefaultGasLimit(token, w3tx); checkConfirm(new Web3Transaction(w3tx.recipient, w3tx.contract, w3tx.value, w3tx.gasPrice, gasEstimate, w3tx.nonce, w3tx.payload, w3tx.description)); }); - - dialog.setSecondaryButtonListener(v -> - { - dialog.dismiss(); - }); - + dialog.setSecondaryButtonListener(v -> dialog.dismiss()); dialog.show(); } private void checkConfirm(Web3Transaction w3tx) { - if (w3tx.gasLimit.equals(BigInteger.ZERO)) - { - estimateError(w3tx); - } - else - { - if (dialog != null && dialog.isShowing()) dialog.dismiss(); - confirmationDialog = new ActionSheetDialog(this, w3tx, token, "", //TODO: Reverse resolve address - w3tx.recipient.toString(), viewModel.getTokenService(), this); - confirmationDialog.setURL("TokenScript"); - confirmationDialog.setCanceledOnTouchOutside(false); - confirmationDialog.show(); - } + if (dialog != null && dialog.isShowing()) dialog.dismiss(); + confirmationDialog = new ActionSheetDialog(this, w3tx, token, "", //TODO: Reverse resolve address + w3tx.recipient.toString(), viewModel.getTokenService(), this); + confirmationDialog.setURL("TokenScript"); + confirmationDialog.setCanceledOnTouchOutside(false); + confirmationDialog.show(); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java index 1f21b7d028..f41b790280 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetsFragment.java @@ -247,7 +247,7 @@ private void forceRedraw() private boolean hasTokenScriptOverride(Token t) { - return viewModel.getAssetDefinitionService().hasTokenView(t.tokenInfo.chainId, t.getAddress(), AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME); + return viewModel.getAssetDefinitionService().hasTokenView(t, AssetDefinitionService.ASSET_SUMMARY_VIEW_NAME); } private TextWatcher setupTextWatcher(NFTAssetsAdapter adapter) diff --git a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java index e3c7479eb6..ccdb925a11 100644 --- a/app/src/main/java/com/alphawallet/app/ui/SendActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/SendActivity.java @@ -78,9 +78,7 @@ public class SendActivity extends BaseActivity implements AmountReadyCallback, StandardFunctionInterface, AddressReadyCallback, ActionSheetCallback { private static final BigDecimal NEGATIVE = BigDecimal.ZERO.subtract(BigDecimal.ONE); - SendViewModel viewModel; - private Wallet wallet; private Token token; private final Handler handler = new Handler(); diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java index fd7fe5e5eb..6b8c7fa7a8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenActivity.java @@ -816,7 +816,7 @@ public void showTokenDetail(Activity activity, Token token) TokenDetailRouter tokenDetailRouter = new TokenDetailRouter(); AssetDefinitionService assetDefinitionService = viewModel.getAssetDefinitionService(); Wallet defaultWallet = viewModel.getWallet(); - boolean hasDefinition = assetDefinitionService.hasDefinition(token.tokenInfo.chainId, token.getAddress()); + boolean hasDefinition = assetDefinitionService.hasDefinition(token); switch (token.getInterfaceSpec()) { case ETHEREUM: diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java index e0eabaf5be..c2c5c312e0 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java @@ -148,7 +148,7 @@ public void handleTokenScriptFunction(String function, List selectio } else { - viewModel.showFunction(this, token, function, selection); + viewModel.showFunction(this, token, function, selection, null); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java index 5cb8de9009..0b2a3dcc86 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java @@ -296,7 +296,7 @@ public void handleTokenScriptFunction(String function, List selectio } else { - viewModel.showFunction(this, token, function, idList); + viewModel.showFunction(this, token, function, idList, null); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenScriptManagementActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenScriptManagementActivity.java index 418e025698..4c7566a41b 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenScriptManagementActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenScriptManagementActivity.java @@ -64,7 +64,7 @@ public void refreshList(boolean refreshScripts) viewModel.getTokenLocatorsLiveData().observe(this, new Observer>() { @Override public void onChanged(List tokenList) { - if (adapter == null) adapter = new TokenScriptManagementAdapter(thisActivity, tokenList, viewModel.getAssetService()); + if (adapter == null) adapter = new TokenScriptManagementAdapter(thisActivity, tokenList, viewModel.getAssetService(), viewModel.getTokensService()); else adapter.refreshList(tokenList); tokenScriptList.setAdapter(adapter); } diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java index fb9f64f95d..c4eebf7229 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java @@ -750,23 +750,7 @@ public boolean onMenuItemClick(MenuItem menuItem) private void initNotificationView(View view) { NotificationView notificationView = view.findViewById(R.id.notification); - boolean hasShownWarning = viewModel.isMarshMallowWarningShown(); - - if (!hasShownWarning && android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) - { - notificationView.setTitle(getContext().getString(R.string.title_version_support_warning)); - notificationView.setMessage(getContext().getString(R.string.message_version_support_warning)); - notificationView.setPrimaryButtonText(getContext().getString(R.string.hide_notification)); - notificationView.setPrimaryButtonListener(() -> - { - notificationView.setVisibility(View.GONE); - viewModel.setMarshMallowWarning(true); - }); - } - else - { - notificationView.setVisibility(View.GONE); - } + notificationView.setVisibility(View.GONE); } @Override diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java index 7b1ac9e0bc..16a0b17ee2 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/NonFungibleTokenAdapter.java @@ -170,7 +170,7 @@ private void setTokenRange(Token t, List tokenIds) int holderType = getHolderType(); //TokenScript view for ERC721 overrides OpenSea display - if (assetService.hasTokenView(t.tokenInfo.chainId, t.getAddress(), ASSET_SUMMARY_VIEW_NAME)) holderType = AssetInstanceScriptHolder.VIEW_TYPE; + if (assetService.hasTokenView(t, ASSET_SUMMARY_VIEW_NAME)) holderType = AssetInstanceScriptHolder.VIEW_TYPE; List sortedList = generateSortedList(assetService, token, tokenIds); //generate sorted list addSortedItems(sortedList, t, holderType); //insert sorted items into view @@ -186,7 +186,7 @@ public void setToken(Token t) int holderType = getHolderType(); //TokenScript view for ERC721 overrides OpenSea display - if (assetService.hasTokenView(t.tokenInfo.chainId, t.getAddress(), ASSET_SUMMARY_VIEW_NAME)) holderType = AssetInstanceScriptHolder.VIEW_TYPE; + if (assetService.hasTokenView(t, ASSET_SUMMARY_VIEW_NAME)) holderType = AssetInstanceScriptHolder.VIEW_TYPE; addRanges(t, holderType); items.endBatchedUpdates(); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenScriptManagementAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenScriptManagementAdapter.java index 166a3b483d..e378b4e590 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenScriptManagementAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokenScriptManagementAdapter.java @@ -17,6 +17,7 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokenscript.TokenScriptFile; import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.service.TokensService; import com.alphawallet.app.ui.TokenScriptManagementActivity; import com.alphawallet.app.widget.AWalletAlertDialog; import com.alphawallet.app.widget.ChainName; @@ -41,14 +42,16 @@ public class TokenScriptManagementAdapter extends RecyclerView.Adapter tokenLocators; private final AssetDefinitionService assetDefinitionService; + private final TokensService tokensService; private AWalletAlertDialog dialog; private final Handler handler = new Handler(); - public TokenScriptManagementAdapter(TokenScriptManagementActivity activity, List locators, AssetDefinitionService assetDefinitionService) { + public TokenScriptManagementAdapter(TokenScriptManagementActivity activity, List locators, AssetDefinitionService assetDefinitionService, TokensService tokensService) { this.context = activity.getBaseContext(); this.activity = activity; this.tokenLocators = new ArrayList<>(locators); this.assetDefinitionService = assetDefinitionService; + this.tokensService = tokensService; inflater = LayoutInflater.from(context); } @@ -80,8 +83,10 @@ public void onBindViewHolder(@NonNull TokenSciptCardHolder tokenSciptCardHolder, address = originContract.addresses.get(chainId).iterator().next(); } + Token token = tokensService.getToken(chainId, address); + //see what the current TS file serving this contract is - TokenScriptFile servingFile = assetDefinitionService.getTokenScriptFile(chainId, address); + TokenScriptFile servingFile = assetDefinitionService.getTokenScriptFile(token); final TokenScriptFile overrideFile = (servingFile != null && !tokenLocator.getFullFileName().equals(servingFile.getAbsolutePath())) ? servingFile : null; if (overrideFile != null) { @@ -108,7 +113,7 @@ public void onBindViewHolder(@NonNull TokenSciptCardHolder tokenSciptCardHolder, tokenSciptCardHolder.tokenFullName.setText(t.getFullName()); } - assetDefinitionService.getSignatureData(chainId, address) + assetDefinitionService.getSignatureData(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig -> onSigData(sig, tokenSciptCardHolder), Throwable::printStackTrace).isDisposed(); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java index ecdbfde41c..eece644efa 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java @@ -52,7 +52,6 @@ public class TokensAdapter extends RecyclerView.Adapter { private static final String TAG = "TKNADAPTER"; - private TokenFilter filterType = TokenFilter.ALL; protected final AssetDefinitionService assetService; protected final TokensService tokensService; diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java index dd6e0b8cd3..52de023ae3 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java @@ -22,6 +22,7 @@ import androidx.core.content.ContextCompat; import com.alphawallet.app.R; +import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.entity.tokendata.TokenTicker; import com.alphawallet.app.entity.tokens.Attestation; @@ -34,6 +35,7 @@ import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.widget.TokenIcon; import com.alphawallet.token.tools.Convert; +import com.alphawallet.token.tools.TokenDefinition; import com.google.android.material.checkbox.MaterialCheckBox; import java.math.BigDecimal; @@ -185,18 +187,13 @@ private void handleAttestation(TokenCardMeta data) { Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getAttestationId()); //TODO: Take name from schema data if available - if (token != null) - { - balanceEth.setText(shortTitle()); - } - else - { - balanceEth.setText(attestation.tokenInfo.name); - } - //BigInteger attestationId = attestation.getAttestationUID(); - balanceCoin.setText(attestation.getAttestationDescription()); + TokenDefinition td = assetDefinition.getAssetDefinition(data.getChain(), data.getAddress()); + NFTAsset nftAsset = new NFTAsset(); + nftAsset.setupScriptElements(td); + balanceEth.setText(attestation.getAttestationName(td)); + balanceCoin.setText(attestation.getAttestationDescription(td)); balanceCoin.setVisibility(View.VISIBLE); - tokenIcon.setIsAttestation(attestation.getSymbol(), data.getChain()); + tokenIcon.setAttestationIcon(nftAsset.getImage(), attestation.getSymbol(), data.getChain()); token = attestation; blankTickerInfo(); } diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 6cc9744904..dd06dceb32 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -1106,8 +1106,10 @@ public static boolean isAlphaWallet(Context context) return context.getPackageName().equals("io.stormbird.wallet"); } - public static boolean hasAttestation(String url) + /*public static boolean hasAttestation(String url) { + result.functionDetail = Utils.decompress(url); + int hashIndex = url.indexOf("#attestation="); if (hashIndex >= 0) { @@ -1121,6 +1123,7 @@ public static boolean hasAttestation(String url) { byte[] tryBase64Data = Base64.decode(url, Base64.DEFAULT); //is this a base64 string? + if (tryBase64Data.length > 0 ) { return true; @@ -1134,7 +1137,7 @@ public static boolean hasAttestation(String url) return false; } - } + }*/ public static String getAttestationString(String url) { @@ -1154,16 +1157,35 @@ public static String getAttestationString(String url) return decoded; } - public static TokenInfo getDefaultAttestationInfo(long chainId) + public static TokenInfo getDefaultAttestationInfo(long chainId, String collectionHash) + { + return new TokenInfo(collectionHash, "EAS Attestation", "ATTN", 0, true, chainId); + } + + public static boolean hasAttestation(String data) { - return new TokenInfo(getEASContract(chainId), "EAS Attestation", "ATTN", 0, true, chainId); + try + { + String inflated = inflateData(getAttestationString(data)); + return inflated.length() > 0; + } + catch (Exception e) + { + return false; + } } public static String decompress(String url) { - Timber.d(toAttestationJson(inflateData(getAttestationString(url)))); - return toAttestationJson(inflateData(getAttestationString(url))); -// return inflateData(getAttestationString(url)); + try + { + //Timber.d(toAttestationJson(inflateData(getAttestationString(url)))); + return toAttestationJson(inflateData(getAttestationString(url))); + } + catch (Exception e) + { + return null; + } } private static String toAttestationJson(String jsonString) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java index 1a7fe54bd7..d3893af46a 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/Erc20DetailViewModel.java @@ -117,7 +117,7 @@ public Realm getRealmInstance(Wallet wallet) public void checkTokenScriptValidity(Token token) { - disposable = assetDefinitionService.getSignatureData(token.tokenInfo.chainId, token.tokenInfo.address) + disposable = assetDefinitionService.getSignatureData(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig::postValue, this::onSigCheckError); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index a8d0861f15..347a005568 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -666,6 +666,13 @@ public void importScriptFile(Context ctx, boolean appExternal, Intent startInten return; //tokenscript with no holding token is currently meaningless. Is this always the case? String newFileName = td.contracts.get(td.holdingToken).addresses.values().iterator().next().iterator().next(); + String holdingContract = td.holdingToken; + TokenDefinition.Attestation attn = td.attestations != null ? td.attestations.get(holdingContract) : null; + if (attn != null) + { + newFileName = newFileName + "-" + attn.chainId; + } + newFileName = newFileName + ".tsml"; if (appExternal) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NFTInfoViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NFTInfoViewModel.java index 3d6ad92f79..3551a5c845 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NFTInfoViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NFTInfoViewModel.java @@ -76,7 +76,7 @@ public void showSendToken(Activity act, Wallet wallet, Token token) public void checkTokenScriptValidity(Token token) { - disposable = assetDefinitionService.getSignatureData(token.tokenInfo.chainId, token.tokenInfo.address) + disposable = assetDefinitionService.getSignatureData(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig::postValue, this::onSigCheckError); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java index 7d4f1095d2..97a1197239 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java @@ -114,7 +114,7 @@ public void checkTokenScriptValidity(Token token) { if (token != null) { - disposable = assetDefinitionService.getSignatureData(token.tokenInfo.chainId, token.tokenInfo.address) + disposable = assetDefinitionService.getSignatureData(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(sig::postValue, this::onSigCheckError); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java index a71481a616..b593def61f 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.Intent; import android.text.TextUtils; +import android.util.Pair; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; @@ -115,6 +116,7 @@ public class TokenFunctionViewModel extends BaseViewModel implements Transaction private final MutableLiveData transactionFinalised = new MutableLiveData<>(); private final MutableLiveData transactionError = new MutableLiveData<>(); private final MutableLiveData gasEstimateComplete = new MutableLiveData<>(); + private final MutableLiveData> gasEstimateError = new MutableLiveData<>(); private final MutableLiveData> traits = new MutableLiveData<>(); private final MutableLiveData assetContract = new MutableLiveData<>(); private final MutableLiveData nftAsset = new MutableLiveData<>(); @@ -208,6 +210,11 @@ public MutableLiveData gasEstimateComplete() return gasEstimateComplete; } + public MutableLiveData> gasEstimateError() + { + return gasEstimateError; + } + public MutableLiveData> traits() { return traits; @@ -270,15 +277,23 @@ public void showTransferToken(Context ctx, Token token, List selecti ctx.startActivity(intent); } - public void showFunction(Context ctx, Token token, String method, List tokenIds) + public void showFunction(Context ctx, Token token, String method, List tokenIds, NFTAsset asset) { Intent intent = new Intent(ctx, FunctionActivity.class); intent.putExtra(C.EXTRA_CHAIN_ID, token.tokenInfo.chainId); intent.putExtra(C.EXTRA_ADDRESS, token.getAddress()); intent.putExtra(C.Key.WALLET, wallet); intent.putExtra(C.EXTRA_STATE, method); - if (tokenIds == null) + if (asset != null) + { + intent.putExtra(C.EXTRA_NFTASSET, asset); + } + + if (tokenIds == null || tokenIds.size() == 0) + { tokenIds = new ArrayList<>(Collections.singletonList(BigInteger.ZERO)); + } + intent.putExtra(C.EXTRA_TOKEN_ID, Utils.bigIntListToString(tokenIds, true)); intent.setFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); ctx.startActivity(intent); @@ -286,7 +301,7 @@ public void showFunction(Context ctx, Token token, String method, List buildNewConfirmation(estimate, w3tx), - error -> buildNewConfirmation(new GasEstimate(BigInteger.ZERO), w3tx)); //node didn't like this tx + error -> { + buildNewConfirmation(new GasEstimate(BigInteger.ZERO), w3tx); //node didn't like this tx + }); } private void buildNewConfirmation(GasEstimate estimate, Web3Transaction w3tx) { - gasEstimateComplete.postValue(new Web3Transaction( - w3tx.recipient, w3tx.contract, w3tx.value, w3tx.gasPrice, estimate.getValue(), w3tx.nonce, w3tx.payload, w3tx.description)); + if (estimate.hasError()) + { + gasEstimateError.postValue(new Pair<>(estimate, w3tx)); + } + else + { + gasEstimateComplete.postValue(new Web3Transaction( + w3tx.recipient, w3tx.contract, w3tx.value, w3tx.gasPrice, estimate.getValue(), w3tx.nonce, w3tx.payload, w3tx.description)); + } } @Override public void showErc20TokenDetail(Activity context, @NotNull String address, String symbol, int decimals, @NotNull Token token) { - boolean hasDefinition = assetDefinitionService.hasDefinition(token.tokenInfo.chainId, address); + boolean hasDefinition = assetDefinitionService.hasDefinition(token); Intent intent = new Intent(context, Erc20DetailActivity.class); intent.putExtra(C.EXTRA_SENDING_TOKENS, !token.isEthereum()); intent.putExtra(C.EXTRA_CONTRACT_ADDRESS, address); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenScriptManagementViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenScriptManagementViewModel.java index eb02418295..40a271a609 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenScriptManagementViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenScriptManagementViewModel.java @@ -4,6 +4,7 @@ import com.alphawallet.app.entity.TokenLocator; import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.service.TokensService; import java.util.List; @@ -17,11 +18,13 @@ public class TokenScriptManagementViewModel extends BaseViewModel { private final AssetDefinitionService assetDefinitionService; + private final TokensService tokensService; private final MutableLiveData> tokenLocatorsLiveData; @Inject - public TokenScriptManagementViewModel(AssetDefinitionService assetDefinitionService) { + public TokenScriptManagementViewModel(AssetDefinitionService assetDefinitionService, TokensService tokensService) { this.assetDefinitionService = assetDefinitionService; + this.tokensService = tokensService; tokenLocatorsLiveData = new MutableLiveData<>(); } @@ -46,4 +49,9 @@ public AssetDefinitionService getAssetService() { return assetDefinitionService; } + + public TokensService getTokensService() + { + return tokensService; + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 523351c961..9c55570c71 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -3,6 +3,8 @@ import static com.alphawallet.app.C.EXTRA_ADDRESS; import static com.alphawallet.app.repository.TokensRealmSource.ADDRESS_FORMAT; import static com.alphawallet.app.widget.CopyTextView.KEY_ADDRESS; +import static com.alphawallet.token.tools.TokenDefinition.NO_SCRIPT; +import static com.alphawallet.token.tools.TokenDefinition.UNCHANGED_SCRIPT; import android.app.Activity; import android.content.ClipData; @@ -59,6 +61,7 @@ import com.alphawallet.app.walletconnect.AWWalletConnectClient; import com.alphawallet.app.widget.WalletFragmentActionsView; import com.alphawallet.token.entity.AttestationValidationStatus; +import com.alphawallet.token.tools.TokenDefinition; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.gson.Gson; @@ -81,6 +84,7 @@ import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import io.realm.RealmResults; +import timber.log.Timber; @HiltViewModel public class WalletViewModel extends BaseViewModel @@ -378,7 +382,7 @@ public TokenGroup getTokenGroup(long chainId, String address) public void showTokenDetail(Activity activity, Token token) { - boolean hasDefinition = assetDefinitionService.hasDefinition(token.tokenInfo.chainId, token.getAddress()); + boolean hasDefinition = assetDefinitionService.hasDefinition(token); switch (token.getInterfaceSpec()) { case ETHEREUM: @@ -673,9 +677,10 @@ public void importEASAttestation(QRResult qrAttn) //validation UID: storeAttestation(easAttn, qrAttn.functionDetail) + .map(this::completeImport) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(attn -> completeImport(easAttn, attn), this::onError) + .subscribe(this::checkTokenScript, this::onError) .isDisposed(); } @@ -726,16 +731,42 @@ private Attestation storeAttestationInternal(EasAttestation attestation, Attesta return attn; } - private void completeImport(EasAttestation attestation, Attestation tokenAttn) + private Token completeImport(Token token) { - if (tokenAttn.isValid() == AttestationValidationStatus.Pass) + if (token instanceof Attestation && ((Attestation)token).isValid() == AttestationValidationStatus.Pass) { - TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, tokenAttn.getAddress(), "1", System.currentTimeMillis(), + Attestation tokenAttn = (Attestation)token; + TokenCardMeta tcmAttestation = new TokenCardMeta(tokenAttn.tokenInfo.chainId, tokenAttn.getAddress(), "1", System.currentTimeMillis(), assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); tcmAttestation.isEnabled = true; updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); } + + return token; + } + + public void checkTokenScript(Token token) + { + //check server for a TokenScript + disposable = assetDefinitionService.checkServerForScript(token, null) + .observeOn(Schedulers.io()) + .subscribeOn(Schedulers.single()) + .subscribe(td -> handleFilename(td, token), Timber::w); + } + + private void handleFilename(TokenDefinition td, Token token) + { + switch (td.nameSpace) + { + case UNCHANGED_SCRIPT: + case NO_SCRIPT: + break; + default: + //found a new script + completeImport(token); + break; + } } public void removeAttestation(Token token) diff --git a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java index 71ad963021..ad0b99841d 100644 --- a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java +++ b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java @@ -416,7 +416,7 @@ public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionS public void displayTicketHolder(Token token, TicketRange range, AssetDefinitionService assetService, ViewType iconified) { //need to wait until the assetDefinitionService has finished loading assets - assetService.getAssetDefinitionASync(token.tokenInfo.chainId, token.tokenInfo.address) + assetService.getAssetDefinitionASync(token) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(td -> renderTicketHolder(token, td, range, assetService, iconified), this::loadingError).isDisposed(); diff --git a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java index 85fdd3e31f..a60399fd45 100644 --- a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java +++ b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java @@ -177,9 +177,9 @@ public void setupAttestationFunctions(StandardFunctionInterface functionInterfac callStandardFunctions = functionInterface; adapter = adp; selection.clear(); + resetButtonCount(); this.token = token; - functions = assetSvs.getAttestationFunctionMap(token.tokenInfo.chainId, token.getAddress(), ((Attestation)token).getAttestationUID()); - //selection.add(((Attestation)token).getAttestationUID()); + functions = assetSvs.getAttestationFunctionMap(token); assetService = assetSvs; showButtons = true; diff --git a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java index 81782346c2..b805edc5ff 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java +++ b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java @@ -468,7 +468,7 @@ public void setGrayscale(boolean grayscale) } } - public void setIsAttestation(String symbol, long chainId) + private void setIsAttestation(String symbol, long chainId) { loadImageFromResource(R.drawable.zero_one); textIcon.setVisibility(View.VISIBLE); @@ -476,4 +476,21 @@ public void setIsAttestation(String symbol, long chainId) textIcon.setText(Utils.getIconisedText(symbol)); setChainIcon(chainId); } + + public void setAttestationIcon(String image, String symbol, long chain) + { + if (!TextUtils.isEmpty(image)) + { + currentRq = Glide.with(this) + .load(image) + .placeholder(R.drawable.zero_one) + .apply(new RequestOptions().circleCrop()) + .listener(requestListener) + .into(new DrawableImageViewTarget(icon)).getRequest(); + } + else + { + setIsAttestation(symbol, chain); + } + } } diff --git a/lib/src/main/java/com/alphawallet/token/entity/TSOriginType.java b/lib/src/main/java/com/alphawallet/token/entity/TSOriginType.java index cdbdfe1789..94efb95b62 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/TSOriginType.java +++ b/lib/src/main/java/com/alphawallet/token/entity/TSOriginType.java @@ -6,5 +6,6 @@ public enum TSOriginType { Contract, - Event + Event, + Attestation } diff --git a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java index a09cbce451..a3f526d93c 100644 --- a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java +++ b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java @@ -33,6 +33,7 @@ import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Bool; import org.web3j.abi.datatypes.Bytes; +import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Bytes32; @@ -47,6 +48,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -63,6 +65,7 @@ public class TokenDefinition protected Locale locale; public final Map contracts = new HashMap<>(); + public final Map attestations = new HashMap<>(); public final Map actions = new HashMap<>(); private Map labels = new HashMap<>(); // store plural etc for token name private final Map namedTypeLookup = new HashMap<>(); //used to protect against name collision @@ -70,7 +73,6 @@ public class TokenDefinition private final Map selections = new HashMap<>(); private final Map activityCards = new HashMap<>(); private final Map structs = new HashMap<>(); - private Attestation attestation = null; public String nameSpace; public TokenscriptContext context; @@ -129,7 +131,7 @@ public List getFunctionData() public Attestation getAttestation() { - return attestation; + return attestations.get(holdingToken); } public EventDefinition parseEvent(Element resolve) throws SAXException @@ -442,7 +444,7 @@ private void extractTags(Element token) throws Exception { case "origins": TSOrigins origin = parseOrigins(element); //parseOrigins(element); - if (origin.isType(TSOriginType.Contract)) holdingToken = origin.getOriginName(); + if (origin.isType(TSOriginType.Contract) || origin.isType(TSOriginType.Attestation)) holdingToken = origin.getOriginName(); break; case "contract": handleAddresses(element); @@ -476,7 +478,8 @@ private void extractTags(Element token) throws Exception } break; case "attestation": - attestation = scanAttestation(element); + Attestation attestation = scanAttestation(element); + attestations.put(attestation.name, attestation); break; default: break; @@ -883,9 +886,9 @@ private void extractSignedInfo(Document xml) { private Attestation scanAttestation(Node attestationNode) throws SAXException { - Attestation attn = new Attestation(); - //NodeList nList; - //nList = xml.getElementsByTagNameNS("https://github.com/TokenScript/attestation", "attestation"); + Element element = (Element) attestationNode; + String name = element.getAttribute("name"); + Attestation attn = new Attestation(name); for (Node n = attestationNode.getFirstChild(); n != null; n = n.getNextSibling()) { @@ -894,18 +897,23 @@ private Attestation scanAttestation(Node attestationNode) throws SAXException switch (attnElement.getLocalName()) { + case "meta": + //read elements of the metadata + attn.addMetaData(attnElement); + break; case "display": handleAttestationDisplay(attnElement); break; - + case "eas": + attn.addAttributes(attnElement); + break; case "struct": case "ProofOfKnowledge": - attn.members.add(parseAttestationStruct(attnElement)); + //attn.members.add(parseAttestationStruct(attnElement)); //attestation.add(parseAttestationStruct(attnElement)); break; - case "origins": - attn.origin = parseOrigins(attnElement); + //attn.origin = parseOrigins(attnElement); //advance to function Element functionElement = getFirstChildElement(attnElement); attn.function = parseFunction(functionElement, Syntax.IA5String); @@ -1059,13 +1067,20 @@ private String getElementName(Node attribute) public AttestationValidation getValidation(List values) { - if (attestation == null || !namedTypeLookup.containsKey(attestation.function.namedTypeReturn)) + //legacy attestations should only have one type + Attestation attn = null; + if (attestations.size() > 0) + { + attn = (Attestation)attestations.values().toArray()[0]; + } + + if (attn == null || !namedTypeLookup.containsKey(attn.function.namedTypeReturn)) { return null; } //get namedType for return - NamedType nType = namedTypeLookup.get(attestation.function.namedTypeReturn); + NamedType nType = namedTypeLookup.get(attn.function.namedTypeReturn); AttestationValidation.Builder builder = new AttestationValidation.Builder(); //find issuerkey @@ -1106,13 +1121,19 @@ public AttestationValidation getValidation(List values) public List> getAttestationReturnTypes() { List> returnTypes = new ArrayList<>(); - if (attestation == null || !namedTypeLookup.containsKey(attestation.function.namedTypeReturn)) + Attestation attn = null; + if (attestations.size() > 0) + { + attn = (Attestation)attestations.values().toArray()[0]; + } + + if (attn == null || !namedTypeLookup.containsKey(attn.function.namedTypeReturn)) { return returnTypes; } //get namedType for return - NamedType nType = namedTypeLookup.get(attestation.function.namedTypeReturn); + NamedType nType = namedTypeLookup.get(attn.function.namedTypeReturn); //add output params for (NamedType.SequenceElement element : nType.sequence) @@ -1127,7 +1148,7 @@ public List> getAttestationReturnTypes() returnTypes.add(new TypeReference() {}); break; case "bytes": - returnTypes.add(new TypeReference() {}); + returnTypes.add(new TypeReference() {}); break; case "string": returnTypes.add(new TypeReference() {}); @@ -1240,15 +1261,66 @@ private enum AttnStructType BOOL, } - public static class Attestation + public class Attestation { - public TSOrigins origin; //single value for validation + //public TSOrigins origin; //single value for validation public FunctionDefinition function = null; - public final List members; + //public final List members; + public Map metadata; + public Map attributes; + public final String name; + public long chainId; - public Attestation() + public Attestation(String name) { - members = new ArrayList<>(); + this.name = name; + metadata = null; + attributes = null; + } + + public void addAttributes(Element element) + { + attributes = new HashMap<>(); + //get schemaUID attribute + String schemaUID = element.getAttribute("schemaUID"); + String networkStr = element.getAttribute("network"); + //this is the backlink to the attestation + ContractInfo info = new ContractInfo("Attestation"); + if (networkStr.length() > 0) + { + this.chainId = Long.parseLong(networkStr); + } + else + { + this.chainId = 1; + } + info.addresses.put(this.chainId, Collections.singletonList(schemaUID)); + contracts.put(this.name, info); + for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() != ELEMENT_NODE) continue; + Element attnElement = (Element) n; + + String name = attnElement.getAttribute("name"); + String text = attnElement.getTextContent(); + + attributes.put(name, text); + } + } + + public void addMetaData(Element element) + { + metadata = new HashMap<>(); + for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() != ELEMENT_NODE) continue; + Element attnElement = (Element) n; + + String metaName = attnElement.getLocalName(); + String metaText = attnElement.getTextContent(); + + metadata.put(metaName, metaText); + } } } @@ -1400,6 +1472,11 @@ private TSOrigins parseOrigins(Element origins) throws SAXException .name(ev.type.name) .event(ev).build(); break; + case "attestation": + String attestationName = element.getAttribute("name"); + tsOrigins = new TSOrigins.Builder(TSOriginType.Attestation) + .name(attestationName).build(); + break; default: throw new SAXException("Unknown Origin Type: '" + element.getLocalName() + "'" ); } From 91a39d1f806397a4e890b75e934aa1358f7734c8 Mon Sep 17 00:00:00 2001 From: James Brown Date: Mon, 3 Jul 2023 22:45:43 +0300 Subject: [PATCH 09/11] Refactor and handle updated spec --- .../app/entity/attestation/Attestation.java | 73 ------- .../app/entity/nftassets/NFTAsset.java | 5 +- .../app/entity/tokens/Attestation.java | 69 +++--- .../alphawallet/app/entity/tokens/Token.java | 12 +- .../app/entity/tokens/TokenCardMeta.java | 5 +- .../tokenscript/TokenscriptFunction.java | 2 +- .../app/router/TokenDetailRouter.java | 8 +- .../app/service/AssetDefinitionService.java | 106 ++++++---- .../app/ui/AssetDisplayActivity.java | 2 +- .../alphawallet/app/ui/FunctionActivity.java | 8 +- .../com/alphawallet/app/ui/HomeActivity.java | 3 +- .../app/ui/NFTAssetDetailActivity.java | 15 +- .../app/ui/TokenDetailActivity.java | 2 +- .../app/ui/TokenFunctionActivity.java | 2 +- .../app/ui/widget/holder/EventHolder.java | 2 +- .../widget/holder/TokenDescriptionHolder.java | 4 +- .../app/ui/widget/holder/TokenHolder.java | 2 +- .../app/viewmodel/HomeViewModel.java | 32 +-- .../app/viewmodel/NFTViewModel.java | 2 +- .../app/viewmodel/TokenFunctionViewModel.java | 4 +- .../app/viewmodel/WalletViewModel.java | 2 +- .../alphawallet/app/web3/Web3TokenView.java | 6 +- .../app/widget/FunctionButtonBar.java | 23 +- .../token/entity/AttestationDefinition.java | 115 ++++++++++ .../token/entity/TSFilterNode.java | 20 +- .../token/tools/TokenDefinition.java | 196 +++--------------- 26 files changed, 343 insertions(+), 377 deletions(-) delete mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/Attestation.java create mode 100644 lib/src/main/java/com/alphawallet/token/entity/AttestationDefinition.java diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/attestation/Attestation.java deleted file mode 100644 index bb80bd27e2..0000000000 --- a/app/src/main/java/com/alphawallet/app/entity/attestation/Attestation.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.alphawallet.app.entity.attestation; - -import java.util.HashSet; - -/** - * Created by JB on 19/01/2023. - */ -public class Attestation -{ - private final String address; - private String name; - private final HashSet supportedChains = new HashSet<>(); - private String subTitle; - private String id; - - public Attestation(String address) - { - this.address = address; - } - - public String databaseKey(String hash) - { - return this.address + "-" + hash; - } - - public boolean isSupportedChain(long chainId) - { - return (supportedChains.isEmpty() || supportedChains.contains(chainId)); - } - - public void addSupportedChain(long chainId) - { - supportedChains.add(chainId); - } - - public String getSubTitle() - { - return subTitle; - } - - public void setSubTitle(String subTitle) - { - this.subTitle = subTitle; - } - - public String getId() - { - return id; - } - - public void setId(String id) - { - this.id = id; - } - - public String getName() - { - return name; - } - - public void setName(String name) - { - this.name = name; - } -} - -//Render attestation: -//1. Parse XML -//2. Extract params from XML -//3. Check Attestation Integrity -//4. Store attestation -//5. Fetch attestation from DB in wallet view - attestation needs to be compatible with TCM ? -//6. Write renderer diff --git a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java index 6ddf0d2061..b71e029a8d 100644 --- a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java @@ -13,6 +13,7 @@ import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.entity.RealmNFTAsset; import com.alphawallet.app.util.Utils; +import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.tools.TokenDefinition; import org.json.JSONArray; @@ -595,7 +596,7 @@ public void addAttribute(String name, String value) public boolean setupScriptElements(TokenDefinition td) { boolean hasMetaData = false; - TokenDefinition.Attestation internalAtt = td != null ? td.getAttestation() : null; + AttestationDefinition internalAtt = td != null ? td.getAttestation() : null; if (internalAtt != null && internalAtt.metadata.size() > 0) { internalAtt.metadata.keySet().forEach(key -> assetMap.put(key, internalAtt.metadata.get(key))); @@ -607,7 +608,7 @@ public boolean setupScriptElements(TokenDefinition td) public void setupScriptAttributes(TokenDefinition td, Token token) { - TokenDefinition.Attestation internalAtt = td.getAttestation(); + AttestationDefinition internalAtt = td.getAttestation(); if (internalAtt != null && internalAtt.attributes != null && internalAtt.attributes.size() > 0) { for (Map.Entry attr : internalAtt.attributes.entrySet()) diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index 2c2cc53440..4452d86d5b 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -12,6 +12,7 @@ import com.alphawallet.app.repository.entity.RealmAttestation; import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.web3j.StructuredDataEncoder; +import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.AttestationValidationStatus; import com.alphawallet.token.entity.TokenScriptResult; @@ -23,6 +24,7 @@ import org.json.JSONObject; import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; +import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import java.math.BigDecimal; @@ -66,9 +68,9 @@ public class Attestation extends Token public Attestation(TokenInfo tokenInfo, String networkName, byte[] attestation) { - super(tokenInfo, BigDecimal.ZERO, System.currentTimeMillis(), networkName, ContractType.ATTESTATION); + super(tokenInfo, BigDecimal.ONE, System.currentTimeMillis(), networkName, ContractType.ATTESTATION); this.attestation = attestation; - setAttributeResult(BigInteger.ZERO, new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, Numeric.toHexString(attestation))); + setAttributeResult(BigInteger.ONE, new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ONE, Numeric.toHexString(attestation))); } public byte[] getAttestation() @@ -167,10 +169,11 @@ public String getAttestationUID() return Numeric.toHexStringNoPrefix(Hash.keccak256(identifier.toString().getBytes(StandardCharsets.UTF_8))); } + @Override public String getAttestationCollectionId() { String eventId = null; - MemberData eventMember = additionalMembers.get(EVENT_ID); + MemberData eventMember = additionalMembers.get(SCHEMA_DATA_PREFIX + EVENT_ID); if (eventMember != null) { eventId = eventMember.getString(); @@ -180,7 +183,8 @@ public String getAttestationCollectionId() //issuer public key //calculate hash from attestation - String hexStr = Numeric.cleanHexPrefix(easAttestation.schema) + "-" + Numeric.cleanHexPrefix(recoverPublicKey(easAttestation)) + (!TextUtils.isEmpty(eventId) ? ("-" + eventId) : ""); + String hexStr = Numeric.cleanHexPrefix(easAttestation.schema).toLowerCase() + Keys.getAddress(recoverPublicKey(easAttestation)).toLowerCase() + + (!TextUtils.isEmpty(eventId) ? eventId : ""); //now convert this into ASCII hex bytes byte[] collectionBytes = hexStr.getBytes(StandardCharsets.UTF_8); //get Hash @@ -189,9 +193,15 @@ public String getAttestationCollectionId() return Numeric.toHexString(hash); } + @Override + public String getTSKey() + { + return getAttestationCollectionId(); + } + public String getAttestationDescription(TokenDefinition td) { - TokenDefinition.Attestation att = td != null ? td.getAttestation() : null; + AttestationDefinition att = td != null ? td.getAttestation() : null; if (att != null && att.attributes != null && att.attributes.size() > 0) { return displayTokenScriptAttrs(att); @@ -202,7 +212,7 @@ public String getAttestationDescription(TokenDefinition td) } } - private String displayTokenScriptAttrs(TokenDefinition.Attestation att) + private String displayTokenScriptAttrs(AttestationDefinition att) { StringBuilder identifier = new StringBuilder(); for (Map.Entry attr : att.attributes.entrySet()) @@ -281,12 +291,18 @@ public void addAssetElements(NFTAsset asset, Context ctx) //add all the attestation members for (Map.Entry m : additionalMembers.entrySet()) { - if (!m.getValue().isSchemaValue() || m.getKey().contains(SCRIPT_URI)) + if (!m.getValue().isSchemaValue() || m.getKey().contains(SCRIPT_URI) || m.getValue().isBytes()) { continue; } - asset.addAttribute(m.getKey(), m.getValue().getString()); + String key = m.getKey(); + if (key.startsWith(SCHEMA_DATA_PREFIX)) + { + key = key.substring(SCHEMA_DATA_PREFIX.length()); + } + + asset.addAttribute(key, m.getValue().getString()); } //now add expiry, issuer key and valid from @@ -314,17 +330,9 @@ public String getAttrValue(String typeName) private void addDateToAttributes(NFTAsset asset, MemberData validFrom, int resource, Context ctx) { - if (validFrom != null) + if (validFrom != null && validFrom.getValue().compareTo(BigInteger.ZERO) > 0) { - String dateFormat = "HH:mm dd MMM yy"; - SimpleDateFormat dateFormatter = new SimpleDateFormat(dateFormat, Locale.ENGLISH); - - long validTime = validFrom.getValue().longValue() * 1000L; - if (validTime > 0) - { - String date = dateFormatter.format(validTime); - asset.addAttribute(ctx.getString(resource), date); - } + asset.addAttribute(ctx.getString(resource), validFrom.getString()); } } @@ -530,7 +538,7 @@ public BigInteger getValue() try { String type = element.getString("type"); - if (type.startsWith("uint") || type.startsWith("int")) + if (type.startsWith("uint") || type.startsWith("int") || type.startsWith("time")) { return BigInteger.valueOf(element.getLong("value")); } @@ -625,8 +633,7 @@ public MemberData setIsTime() private String formatDate(long time) { DateFormat f = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault()); - String formattedDate = f.format(time*1000); - return formattedDate; + return f.format(time*1000); } public boolean isBytes() @@ -645,12 +652,6 @@ public boolean isBytes() } } - /*@Override - public String getTSKey() - { - return tokenInfo.address.toLowerCase() + "-" + tokenInfo.chainId + "-" + getAttestationUID(); - }*/ - @Override public Single getScriptURI() { @@ -716,4 +717,18 @@ private static String recoverPublicKey(EasAttestation attestation) return recoveredKey; } + + public BigInteger getUUID() + { + if (isEAS()) + { + byte[] hash = Hash.keccak256(Numeric.hexStringToByteArray(getEasAttestation().data)); + return Numeric.toBigInt(hash); + } + else + { + //TODO: Support ASN.1 Attestations + return BigInteger.ONE; + } + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index 25bde8c07e..9872e78c07 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -663,7 +663,7 @@ public String getTokenName(AssetDefinitionService assetService, int count) //see if this token is covered by any contract if (assetService != null && assetService.hasDefinition(this)) { - TokenDefinition td = assetService.getAssetDefinition(tokenInfo.chainId, getAddress()); + TokenDefinition td = assetService.getAssetDefinition(this); if (td != null) { name = td.getTokenName(count); @@ -1119,4 +1119,14 @@ public String getAttrValue(String typeName) { return ""; } + + public BigInteger getUUID() + { + return BigInteger.ONE; + } + + public String getAttestationCollectionId() + { + return getTSKey(); + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java index 7242d11f31..d9e0a192cc 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenCardMeta.java @@ -65,8 +65,7 @@ public String getAttestationId() { int sepIndex = tokenId.indexOf("-"); sepIndex = tokenId.indexOf("-", sepIndex+1); - String attnId = tokenId.substring(sepIndex+1, tokenId.length() - 4); - return attnId; + return tokenId.substring(sepIndex+1, tokenId.length() - 4); } else { @@ -200,7 +199,7 @@ public BigInteger getTokenID() private long calculateTokenNameWeight(long chainId, String tokenAddress, AssetDefinitionService svs, String tokenName, String symbol, boolean isEth, TokenGroup group, int attnId) { - int weight = 1000; //ensure base eth types are always displayed first + long weight = 1000; //ensure base eth types are always displayed first String name = svs != null ? svs.getTokenName(chainId, tokenAddress, 1) : null; if (name != null) { diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java index 50deaba959..e9776cb79e 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java @@ -894,7 +894,7 @@ else if (!TextUtils.isEmpty(element.value)) public Single fetchAttrResult(Token token, Attribute attr, BigInteger tokenId, TokenDefinition td, AttributeInterface attrIf, ViewType itemView) { - final BigInteger useTokenId = (attr == null || !attr.usesTokenId()) ? BigInteger.ZERO : tokenId; + final BigInteger useTokenId = (attr == null) ? BigInteger.ZERO : tokenId; if (attr == null) { return Single.fromCallable(() -> new TokenScriptResult.Attribute("bd", "bd", BigInteger.ZERO, "")); diff --git a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java index 265e8c696f..28c04e722b 100644 --- a/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java +++ b/app/src/main/java/com/alphawallet/app/router/TokenDetailRouter.java @@ -68,13 +68,13 @@ public void openLegacyToken(Activity context, Token token, Wallet wallet) context.startActivityForResult(intent, C.TERMINATE_ACTIVITY); } - public void openAttestation(Activity context, long chainId, String address, Wallet wallet, NFTAsset asset) + public void openAttestation(Activity context, Token token, Wallet wallet, NFTAsset asset) { Intent intent = new Intent(context, NFTAssetDetailActivity.class); intent.putExtra(C.Key.WALLET, wallet); - intent.putExtra(C.EXTRA_CHAIN_ID, chainId); - intent.putExtra(C.EXTRA_ADDRESS, address); - intent.putExtra(C.EXTRA_TOKEN_ID, "1"); + intent.putExtra(C.EXTRA_CHAIN_ID, token.tokenInfo.chainId); + intent.putExtra(C.EXTRA_ADDRESS, token.tokenInfo.address); + intent.putExtra(C.EXTRA_TOKEN_ID, token.getUUID().toString()); intent.putExtra(C.EXTRA_ATTESTATION_ID, asset.getAttestationID()); intent.putExtra(C.EXTRA_NFTASSET, asset); context.startActivityForResult(intent, C.TERMINATE_ACTIVITY); diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index 0bf7a87cfb..2fd4ef8a11 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -53,6 +53,7 @@ import com.alphawallet.ethereum.EthereumNetworkBase; import com.alphawallet.ethereum.NetworkInfo; import com.alphawallet.token.entity.ActionModifier; +import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.AttributeInterface; import com.alphawallet.token.entity.ContractAddress; @@ -87,8 +88,6 @@ import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Uint256; -import org.web3j.abi.datatypes.generated.Uint64; -import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; @@ -139,7 +138,6 @@ import io.realm.exceptions.RealmException; import io.realm.exceptions.RealmPrimaryKeyConstraintException; import timber.log.Timber; -import wallet.core.jni.Hash; /** @@ -158,6 +156,7 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface private static final String EIP5169_ISSUER = "EIP5169-IPFS"; private static final String EIP5169_CERTIFIER = "Smart Token Labs"; private static final String EIP5169_KEY_OWNER = "Contract Owner"; //TODO Source this from the contract via owner() + private static final String TS_EXTENSION = ".tsml"; private final Context context; private final IPFSServiceType ipfsService; @@ -715,7 +714,7 @@ public Single resetAttributes(TokenDefinition td) */ public Single refreshAllAttributes(Token token) { - TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition td = getAssetDefinition(token); if (td == null) return Single.fromCallable(() -> false); return Single.fromCallable(() -> { @@ -775,7 +774,7 @@ private Attribute getTypeFromList(String key, List attrList) public TokenScriptResult getTokenScriptResult(Token token, BigInteger tokenId) { TokenScriptResult result = new TokenScriptResult(); - TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition definition = getAssetDefinition(token); if (definition != null) { for (String key : definition.attributes.keySet()) @@ -824,7 +823,7 @@ else if (attrtype.function != null) public TokenScriptResult.Attribute getAttribute(Token token, BigInteger tokenId, String attribute) { - TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition definition = getAssetDefinition(token); if (definition != null && definition.attributes.containsKey(attribute)) { return getTokenscriptAttr(definition, tokenId, attribute); @@ -912,18 +911,25 @@ public TokenScriptFile getTokenScriptFile(long chainId, String address) return new TokenScriptFile(context); } + public String getDebugPath(String fileName) + { + return context.getExternalFilesDir("") + File.separator + fileName; + } + public TokenScriptFile getTokenScriptFile(Token token) { - try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) + String fileName = token.getTSKey() + TS_EXTENSION; + //first try debug file + TokenScriptFile tsf = new TokenScriptFile(context, getDebugPath(fileName)); + if (tsf.exists()) { - RealmTokenScriptData tsData = realm.where(RealmTokenScriptData.class) - .equalTo("instanceKey", token.getTSKey()) - .findFirst(); + return tsf; + } - if (tsData != null && tsData.getFilePath() != null) - { - return new TokenScriptFile(context, tsData.getFilePath()); - } + File f = new File(context.getFilesDir(), fileName); + if (f.exists()) + { + return new TokenScriptFile(context, f.getAbsolutePath()); } return new TokenScriptFile(context); @@ -954,6 +960,26 @@ public TokenDefinition getAssetDefinition(long chainId, String address) return assetDef; // if nothing found use default } + public TokenDefinition getAssetDefinition(Token token) + { + if (token == null) + { + return null; + } + + try + { + TokenScriptFile tsf = getTokenScriptFile(token); + return parseFile(tsf.getInputStream()); + } + catch (Exception e) + { + // NOP + } + + return null; + } + public Single getAssetDefinitionASync(long chainId, String address) { if (address == null) @@ -1243,6 +1269,8 @@ private String compareExistingScript(Token token, String uri) { String entryKey = token.getTSKey(); //getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); + TokenScriptFile tsf = getTokenScriptFile(token); + RealmTokenScriptData entry = realm.where(RealmTokenScriptData.class) .equalTo("instanceKey", entryKey) .findFirst(); @@ -1250,7 +1278,8 @@ private String compareExistingScript(Token token, String uri) if (entry != null && !TextUtils.isEmpty(entry.getFileHash()) && !TextUtils.isEmpty(entry.getFilePath()) && !TextUtils.isEmpty(entry.getIpfsPath()) - && entry.getIpfsPath().equals(uri)) + && entry.getIpfsPath().equals(uri) + && tsf.exists()) { returnUri = UNCHANGED_SCRIPT; } @@ -1815,7 +1844,7 @@ private boolean isAddress(File file) private File getDownloadedXMLFile(String contractAddress) { //if in secure area will simply be address + XML - String filename = contractAddress + ".xml"; + String filename = contractAddress + TS_EXTENSION; File file = new File(context.getFilesDir(), filename); if (file.exists() && file.canRead()) { @@ -1963,7 +1992,7 @@ private File storeFile(String address, Pair result) throws IOEx { if (result.first == null || result.first.length() < 10) return new File(""); - String fName = address + ".xml"; + String fName = address + TS_EXTENSION; //Store received files in the internal storage area - no need to ask for permissions File file = new File(context.getFilesDir(), fName); @@ -2019,10 +2048,10 @@ public boolean hasTokenView(Token token, String type) } } - public String getTokenView(long chainId, String contractAddr, String type) + public String getTokenView(Token token, String type) { String viewHTML = ""; - TokenDefinition td = getAssetDefinition(chainId, contractAddr); + TokenDefinition td = getAssetDefinition(token); if (td != null) { viewHTML = td.getTokenView(type); @@ -2031,10 +2060,10 @@ public String getTokenView(long chainId, String contractAddr, String type) return viewHTML; } - public String getTokenViewStyle(long chainId, String contractAddr, String type) + public String getTokenViewStyle(Token token, String type) { String styleData = ""; - TokenDefinition td = getAssetDefinition(chainId, contractAddr); + TokenDefinition td = getAssetDefinition(token); if (td != null) { styleData = td.getTokenViewStyle(type); @@ -2043,9 +2072,9 @@ public String getTokenViewStyle(long chainId, String contractAddr, String type) return styleData; } - public List getTokenViewLocalAttributes(long chainId, String contractAddr) + public List getTokenViewLocalAttributes(Token token) { - TokenDefinition td = getAssetDefinition(chainId, contractAddr); + TokenDefinition td = getAssetDefinition(token); List results = new ArrayList<>(); if (td != null) { @@ -2056,9 +2085,14 @@ public List getTokenViewLocalAttributes(long chainId, String contract return results; } - public Map getTokenFunctionMap(long chainId, String contractAddr) + public Map getTokenFunctionMap(Token token) { - TokenDefinition td = getAssetDefinition(chainId, contractAddr); + if (token.getInterfaceSpec() == ContractType.ATTESTATION) + { + return getAttestationFunctionMap(token); + } + + TokenDefinition td = getAssetDefinition(token); if (td != null) { return td.getActions(); @@ -2109,7 +2143,7 @@ public Single>> fetchFunctionMap(Token token, @NotN return Single.fromCallable(() -> { ActionModifier requiredActionModifier = type == ContractType.ATTESTATION ? ActionModifier.ATTESTATION : ActionModifier.NONE; Map> validActions = new HashMap<>(); - TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress()); + TokenDefinition td = getAssetDefinition(token); if (td != null) { Map actions = td.getActions(); @@ -2170,10 +2204,10 @@ private void addIntrinsicAttributes(Map att public String checkFunctionDenied(Token token, String actionName, List tokenIds) { String denialMessage = null; - TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress()); + TokenDefinition td = getAssetDefinition(token); if (td != null) { - BigInteger tokenId = tokenIds != null ? tokenIds.get(0) : BigInteger.ZERO; + BigInteger tokenId = (tokenIds != null && tokenIds.size() > 0) ? tokenIds.get(0) : BigInteger.ZERO; TSAction action = td.actions.get(actionName); TSSelection selection = action.exclude != null ? td.getSelection(action.exclude) : null; if (selection != null) @@ -2302,7 +2336,7 @@ public void onEvent(int event, @Nullable String file) case MODIFY: try { - if (file.contains(".xml") || file.contains(".tsml")) + if (file.contains(TS_EXTENSION)) { final File newTsFile = new File(listenerPath, file); handleNewTSFile(newTsFile) @@ -2581,7 +2615,7 @@ public StringBuilder getTokenAttrs(Token token, BigInteger tokenId, int count) { StringBuilder attrs = new StringBuilder(); - TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition definition = getAssetDefinition(token); String label = token.getTokenTitle(); if (definition != null && definition.getTokenName(1) != null) { @@ -2641,7 +2675,7 @@ public void clearResultMap() public Observable resolveAttrs(Token token, TokenDefinition td, BigInteger tokenId, List extraAttrs, ViewType itemView) { - TokenDefinition definition = td != null ? td : getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition definition = td != null ? td : getAssetDefinition(token); ContractAddress cAddr = new ContractAddress(token.tokenInfo.chainId, token.tokenInfo.address); if (definition == null) return Observable.fromCallable(() -> new TokenScriptResult.Attribute("RAttrs", "", BigInteger.ZERO, "")); @@ -2722,7 +2756,7 @@ public List getAttestationAttrs(Token token, TSActi public Map getAttestationFunctionMap(Token att) { - TokenDefinition td = getAssetDefinition(att.tokenInfo.chainId, att.tokenInfo.address); + TokenDefinition td = getAssetDefinition(att); Map actions = new HashMap<>(); if (att != null && td != null) { @@ -2750,7 +2784,7 @@ private Observable resolveAttrs(Token token, BigInt public Observable resolveAttrs(Token token, List tokenIds, List extraAttrs) { - TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition definition = getAssetDefinition(token); if (definition == null) { return Observable.fromCallable(() -> new TokenScriptResult.Attribute("", "", BigInteger.ZERO, "")); @@ -2806,7 +2840,7 @@ private List getLocalTSMLFiles() public String generateTransactionPayload(Token token, BigInteger tokenId, FunctionDefinition def) { - TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition td = getAssetDefinition(token); if (td == null) return ""; Function function = tokenscriptUtility.generateTransactionFunction(token, tokenId, td, def, this); if (function.getInputParameters() == null) @@ -2853,7 +2887,7 @@ public String convertInputValue(Attribute attr, String valueFromInput) public String resolveReference(@NotNull Token token, TSAction action, TokenscriptElement arg, BigInteger tokenId) { - TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress()); + TokenDefinition td = getAssetDefinition(token); return tokenscriptUtility.resolveReference(token, arg, tokenId, td, this); } @@ -3084,7 +3118,7 @@ public Attestation validateAttestation(String attestation, TokenInfo tInfo) att.setTokenWallet(tokensService.getCurrentAddress()); //call validation function and get details - TokenDefinition.Attestation definitionAtt = td.getAttestation(); + AttestationDefinition definitionAtt = td.getAttestation(); //can we get the details? if (definitionAtt != null && definitionAtt.function != null) diff --git a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java index bf1284ba5b..31ff8f4fc7 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AssetDisplayActivity.java @@ -338,7 +338,7 @@ public void handleFunctionDenied(String denialMessage) public void handleTokenScriptFunction(String function, List selection) { //does the function have a view? If it's transaction only then handle here - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); TSAction action = functions.get(function); token.clearResultMap(); diff --git a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java index caa3736ee3..74fb0afe53 100644 --- a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java @@ -161,7 +161,7 @@ private void displayFunction(String tokenAttrs) { try { - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); TSAction action = functions.get(actionMethod); String magicValues = viewModel.getAssetDefinitionService().getMagicValuesForInjection(token.tokenInfo.chainId); @@ -192,7 +192,7 @@ private void getAttrs() } // Fetch attributes local to this action and add them to the injected token properties - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); if (functions == null) { recreate(); @@ -214,7 +214,7 @@ private void getAttrs() private void addMultipleTokenIds(StringBuilder sb) { - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); TSAction action = functions.get(actionMethod); boolean hasTokenIds = false; @@ -329,7 +329,7 @@ public boolean onOptionsItemSelected(MenuItem item) { private void completeTokenScriptFunction(String function) { - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); if (functions == null) return; action = functions.get(function); diff --git a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java index d4abd51651..faf8c4a83c 100644 --- a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java @@ -1171,8 +1171,7 @@ else if (importData != null && importData.startsWith("wc:")) } else if (importPath != null) { - boolean useAppExternalDir = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || !viewModel.checkDebugDirectory(); - viewModel.importScriptFile(this, useAppExternalDir, startIntent); + viewModel.importScriptFile(this, startIntent); } } catch (SalesOrderMalformed s) diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index f57a731c4c..e63f704fb5 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -389,14 +389,7 @@ private void setupFunctionBar(Wallet wallet) if (BuildConfig.DEBUG || wallet.type != WalletType.WATCH) { FunctionButtonBar functionBar = findViewById(R.id.layoutButtons); - if (asset != null && asset.isAttestation()) - { - functionBar.setupAttestationFunctions(this, viewModel.getAssetDefinitionService(), token, null); - } - else - { - functionBar.setupFunctions(this, viewModel.getAssetDefinitionService(), token, null, Collections.singletonList(tokenId)); - } + functionBar.setupFunctions(this, viewModel.getAssetDefinitionService(), token, null, Collections.singletonList(tokenId)); functionBar.revealButtons(); functionBar.setWalletType(wallet.type); } @@ -409,7 +402,7 @@ private void completeAttestationTokenScriptSetup(TSAction action) { for (TokenScriptResult.Attribute attr : attestationAttrs) { - token.setAttributeResult(BigInteger.ONE, attr); + token.setAttributeResult(tokenId, attr); } } } @@ -626,7 +619,7 @@ private void onOpenSeaAsset(OpenSeaAsset openSeaAsset) private void setupAttestation() { NFTAsset attnAsset = new NFTAsset(); - TokenDefinition td = viewModel.getAssetDefinitionService().getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition td = viewModel.getAssetDefinitionService().getAssetDefinition(token); if (td != null) { attnAsset.setupScriptElements(td); @@ -704,7 +697,7 @@ public void showTransferToken(List selection) public void handleTokenScriptFunction(String function, List selection) { //does the function have a view? If it's transaction only then handle here - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); if (functions == null) return; TSAction action = functions.get(function); token.clearResultMap(); diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java index c2c5c312e0..34e9bfca6d 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenDetailActivity.java @@ -134,7 +134,7 @@ public void showTransferToken(List selection) public void handleTokenScriptFunction(String function, List selection) { //does the function have a view? If it's transaction only then handle here - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); TSAction action = functions.get(function); //handle TS function diff --git a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java index 0b2a3dcc86..d836644a77 100644 --- a/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/TokenFunctionActivity.java @@ -276,7 +276,7 @@ public void handleFunctionDenied(String denialMessage) @Override public void handleTokenScriptFunction(String function, List selection) { - Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + Map functions = viewModel.getAssetDefinitionService().getTokenFunctionMap(token); TSAction action = functions.get(function); if (action != null && action.view == null && action.function != null) { diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/EventHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/EventHolder.java index 22735d06d5..7801a075e7 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/EventHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/EventHolder.java @@ -104,7 +104,7 @@ public void bind(@Nullable EventMeta data, @NonNull Bundle addition) tokenIcon.bindData(token, assetDefinition); String itemView = null; - TokenDefinition td = assetDefinition.getAssetDefinition(eventData.getChainId(), eventData.getTokenAddress()); + TokenDefinition td = assetDefinition.getAssetDefinition(token); if (td != null && td.getActivityCards().containsKey(eventData.getFunctionId())) { TSTokenView view = td.getActivityCards().get(eventData.getFunctionId()).getView(ASSET_SUMMARY_VIEW_NAME); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenDescriptionHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenDescriptionHolder.java index b83eefd2cc..8259e7bcb8 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenDescriptionHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenDescriptionHolder.java @@ -50,9 +50,9 @@ public TokenDescriptionHolder(int resId, ViewGroup parent, Token t, AssetDefinit public void bind(@Nullable Token token, @NonNull Bundle addition) { count.setText(String.valueOf(assetCount)); String tokenName = token.tokenInfo.name; - if (assetService.getAssetDefinition(token.tokenInfo.chainId, token.getAddress()) != null) + if (assetService.getAssetDefinition(token) != null) { - String nameCandidate = assetService.getAssetDefinition(token.tokenInfo.chainId, token.getAddress()).getTokenName(token.getTokenCount()); + String nameCandidate = assetService.getAssetDefinition(token).getTokenName(token.getTokenCount()); if (nameCandidate != null && nameCandidate.length() > 0) tokenName = nameCandidate; } title.setText(tokenName); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java index 52de023ae3..27dd850cb6 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java @@ -187,7 +187,7 @@ private void handleAttestation(TokenCardMeta data) { Attestation attestation = (Attestation) tokensService.getAttestation(data.getChain(), data.getAddress(), data.getAttestationId()); //TODO: Take name from schema data if available - TokenDefinition td = assetDefinition.getAssetDefinition(data.getChain(), data.getAddress()); + TokenDefinition td = assetDefinition.getAssetDefinition(attestation); NFTAsset nftAsset = new NFTAsset(); nftAsset.setupScriptElements(td); balanceEth.setText(attestation.getAttestationName(td)); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index 347a005568..62619bd874 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -72,7 +72,10 @@ import com.alphawallet.app.widget.EmailPromptView; import com.alphawallet.app.widget.QRCodeActionsView; import com.alphawallet.app.widget.WhatsNewView; +import com.alphawallet.token.entity.AttestationDefinition; +import com.alphawallet.token.entity.ContractInfo; import com.alphawallet.token.entity.MagicLinkData; +import com.alphawallet.token.tools.Numeric; import com.alphawallet.token.tools.ParseMagicLink; import com.alphawallet.token.tools.TokenDefinition; import com.google.android.material.bottomsheet.BottomSheetBehavior; @@ -80,9 +83,12 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import org.web3j.crypto.Keys; + import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -101,6 +107,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import timber.log.Timber; +import wallet.core.jni.Hash; @HiltViewModel public class HomeViewModel extends BaseViewModel @@ -654,7 +661,7 @@ private TokenDefinition parseFile(Context ctx, InputStream xmlInputStream) throw xmlInputStream, locale, null); } - public void importScriptFile(Context ctx, boolean appExternal, Intent startIntent) + public void importScriptFile(Context ctx, Intent startIntent) { Uri uri = startIntent.getData(); final ContentResolver contentResolver = ctx.getContentResolver(); @@ -665,25 +672,26 @@ public void importScriptFile(Context ctx, boolean appExternal, Intent startInten if (td.holdingToken == null || td.holdingToken.length() == 0) return; //tokenscript with no holding token is currently meaningless. Is this always the case? - String newFileName = td.contracts.get(td.holdingToken).addresses.values().iterator().next().iterator().next(); String holdingContract = td.holdingToken; - TokenDefinition.Attestation attn = td.attestations != null ? td.attestations.get(holdingContract) : null; - if (attn != null) - { - newFileName = newFileName + "-" + attn.chainId; - } + AttestationDefinition attn = td.attestations != null ? td.attestations.get(holdingContract) : null; - newFileName = newFileName + ".tsml"; - - if (appExternal) + //determine type of holding token + String newFileName = td.contracts.get(td.holdingToken).addresses.values().iterator().next().iterator().next(); + ContractInfo info = td.contracts.get(td.holdingToken); + if (attn != null && info.contractInterface.equals("Attestation")) { - newFileName = ctx.getExternalFilesDir("") + File.separator + newFileName; + //calculate using formula: #{scheme.drop0x}#{address.drop0x.lowercased}#{eventId} + String address = Numeric.cleanHexPrefix(Numeric.toHexString(Keys.getAddress(attn.issuerKey))).toLowerCase(); + String preHash = Numeric.cleanHexPrefix(newFileName).toLowerCase() + address + (!TextUtils.isEmpty(attn.terminationId) ? attn.terminationId : ""); + newFileName = Numeric.toHexString(Hash.keccak256(preHash.getBytes(StandardCharsets.UTF_8))); } else { - newFileName = Environment.getExternalStorageDirectory() + File.separator + ALPHAWALLET_DIR + File.separator + newFileName; + newFileName = td.contracts.get(td.holdingToken).addresses.values().iterator().next().iterator().next(); } + newFileName = assetDefinitionService.getDebugPath(newFileName + ".tsml"); + //Store the new Definition try (FileOutputStream fos = new FileOutputStream(newFileName)) { diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java index 97a1197239..3ab1f5271c 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NFTViewModel.java @@ -210,7 +210,7 @@ public void onDestroy() public boolean hasTokenScript(Token token) { - return token != null && assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address) != null; + return token != null && assetDefinitionService.getAssetDefinition(token) != null; } public void updateAttributes(Token token) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java index b593def61f..71f00f7681 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java @@ -915,7 +915,7 @@ public String getBrowserRPC(long chainId) public boolean hasTokenScript(Token token) { - return token != null && assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address) != null; + return token != null && assetDefinitionService.getAssetDefinition(token) != null; } public void updateLocalAttributes(Token token, BigInteger tokenId) @@ -934,7 +934,7 @@ private void updateAllowedAttrs(Token token, Map> avail { return; } - TokenDefinition td = assetDefinitionService.getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address); + TokenDefinition td = assetDefinitionService.getAssetDefinition(token); List localAttrList = assetDefinitionService.getLocalAttributes(td, availableActions); //now refresh all these attrs diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 9c55570c71..52e9b2a7e6 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -401,7 +401,7 @@ public void showTokenDetail(Activity activity, Token token) break; case ATTESTATION: - tokenDetailRouter.openAttestation(activity, token.tokenInfo.chainId, token.getAddress(), defaultWallet.getValue(), new NFTAsset((Attestation)token)); + tokenDetailRouter.openAttestation(activity, token, defaultWallet.getValue(), new NFTAsset((Attestation)token)); break; case ERC721: diff --git a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java index ad0b99841d..107ff6a739 100644 --- a/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java +++ b/app/src/main/java/com/alphawallet/app/web3/Web3TokenView.java @@ -462,7 +462,7 @@ public void renderTokenscriptView(Token token, TicketRange range, AssetDefinitio final StringBuilder attrs = assetService.getTokenAttrs(token, tokenId, range.tokenIds.size()); - assetService.resolveAttrs(token, null, tokenId, assetService.getTokenViewLocalAttributes(token.tokenInfo.chainId, token.tokenInfo.address), itemView) + assetService.resolveAttrs(token, null, tokenId, assetService.getTokenViewLocalAttributes(token), itemView) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(attr -> onAttr(attr, attrs), throwable -> onError(token, throwable, range), @@ -493,9 +493,9 @@ private void displayTicket(Token token, AssetDefinitionService assetService, Str break; } - String view = assetService.getTokenView(token.tokenInfo.chainId, token.getAddress(), viewName); + String view = assetService.getTokenView(token, viewName); if (TextUtils.isEmpty(view)) view = buildViewError(token, range, viewName); - String style = assetService.getTokenViewStyle(token.tokenInfo.chainId, token.getAddress(), viewName); + String style = assetService.getTokenViewStyle(token, viewName); unencodedPage = injectWeb3TokenInit(view, attrs.toString(), range.tokenIds.get(0)); unencodedPage = injectStyleAndWrapper(unencodedPage, style); //style injected last so it comes first diff --git a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java index a60399fd45..6950134e31 100644 --- a/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java +++ b/app/src/main/java/com/alphawallet/app/widget/FunctionButtonBar.java @@ -166,29 +166,24 @@ public void setupFunctions(StandardFunctionInterface functionInterface, AssetDef adapter = adp; selection.clear(); if (tokenIds != null) selection.addAll(tokenIds); + resetButtonCount(); this.token = token; - functions = assetSvs.getTokenFunctionMap(token.tokenInfo.chainId, token.getAddress()); + functions = assetSvs.getTokenFunctionMap(token); assetService = assetSvs; getFunctionMap(assetSvs, token.getInterfaceSpec()); } - public void setupAttestationFunctions(StandardFunctionInterface functionInterface, AssetDefinitionService assetSvs, Token token, NonFungibleAdapterInterface adp) + public void setupAttestationFunctions(StandardFunctionInterface functionInterface, AssetDefinitionService assetSvs, Token token, NonFungibleAdapterInterface adp, List tokenIds) { callStandardFunctions = functionInterface; adapter = adp; selection.clear(); + selection.addAll(tokenIds); resetButtonCount(); this.token = token; functions = assetSvs.getAttestationFunctionMap(token); assetService = assetSvs; - showButtons = true; - - for (Map.Entry entry : functions.entrySet()) - { - addFunction(new ItemClick(entry.getKey(), 0)); - } - - showButtons(); + getFunctionMap(assetSvs, token.getInterfaceSpec()); } /** @@ -586,7 +581,7 @@ private void populateButtons(Token token, BigInteger tokenId) addTokenScriptFunctions(availableFunctions, token, tokenId); //If Token is Non-Fungible then display the custom functions first - usually these are more frequently used - if (!token.isNonFungible()) + if (!token.isNonFungible() && token.getInterfaceSpec() != ContractType.ATTESTATION) { addStandardTokenFunctions(token); } @@ -607,7 +602,7 @@ private void populateButtons(Token token, BigInteger tokenId) findViewById(R.id.layoutButtons).setVisibility(View.GONE); - if (!token.isNonFungible()) + if (!token.isNonFungible() && token.getInterfaceSpec() != ContractType.ATTESTATION) { addFunction(new ItemClick(context.getString(R.string.generate_payment_request), R.string.generate_payment_request)); } @@ -615,7 +610,7 @@ private void populateButtons(Token token, BigInteger tokenId) private void addTokenScriptFunctions(Map availableFunctions, Token token, BigInteger tokenId) { - TokenDefinition td = assetService.getAssetDefinition(token.tokenInfo.chainId, token.getAddress()); + TokenDefinition td = assetService.getAssetDefinition(token); if (td != null && tokenId != null && functions != null) { @@ -650,7 +645,7 @@ private void addTokenScriptFunctions(Map availableFunctions, T */ private boolean setupCustomTokenActions() { - if (token.tokenInfo.chainId == POLYGON_ID && token.isNonFungible()) + if (token.tokenInfo.chainId == POLYGON_ID && token.isNonFungible() || token.getInterfaceSpec() == ContractType.ATTESTATION) { return false; } diff --git a/lib/src/main/java/com/alphawallet/token/entity/AttestationDefinition.java b/lib/src/main/java/com/alphawallet/token/entity/AttestationDefinition.java new file mode 100644 index 0000000000..fd370d650f --- /dev/null +++ b/lib/src/main/java/com/alphawallet/token/entity/AttestationDefinition.java @@ -0,0 +1,115 @@ +package com.alphawallet.token.entity; + +import static org.w3c.dom.Node.ELEMENT_NODE; + +import com.alphawallet.token.entity.ContractInfo; +import com.alphawallet.token.entity.FunctionDefinition; +import com.alphawallet.token.tools.Numeric; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by JB on 19/01/2023. + */ + +public class AttestationDefinition +{ + //public TSOrigins origin; //single value for validation + public FunctionDefinition function = null; + //public final List members; + public Map metadata; + public Map attributes; + public final String name; + public long chainId; + public byte[] issuerKey; //also used to generate collectionId + public String terminationId; //used to generate collectionId + public String replacementFieldId; //used to check if new attestation should replace the old + + public AttestationDefinition(String name) + { + this.name = name; + metadata = null; + attributes = null; + } + + public void handleEventId(Element element) + { + terminationId = element.getTextContent(); + } + + public void handleReplacementField(Element element) + { + replacementFieldId = element.getTextContent(); + } + + public void handleKey(Element element) + { + //should be the key itself + String key = Numeric.cleanHexPrefix(element.getTextContent()); + if (key.length() == 130 && key.startsWith("04")) + { + key = key.substring(2); + } + + issuerKey = Numeric.hexStringToByteArray(key); + } + + public ContractInfo addAttributes(Element element) + { + //get schemaUID attribute + String schemaUID = element.getAttribute("schemaUID"); + String networkStr = element.getAttribute("network"); + //this is the backlink to the attestation + ContractInfo info = new ContractInfo("Attestation"); + if (networkStr.length() > 0) + { + this.chainId = Long.parseLong(networkStr); + } + else + { + this.chainId = 1; + } + info.addresses.put(this.chainId, Collections.singletonList(schemaUID)); + //contracts.put(this.name, info); + for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() != ELEMENT_NODE) continue; + Element attnElement = (Element) n; + + String name = attnElement.getAttribute("name"); + String text = attnElement.getTextContent(); + + attributes.put(name, text); + } + + return info; + } + + public void addMetaData(Element element) + { + metadata = new HashMap<>(); + attributes = new HashMap<>(); + for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() != ELEMENT_NODE) continue; + Element attnElement = (Element) n; + + String metaText = attnElement.getTextContent(); + String metaName = attnElement.getLocalName(); + if (metaName.equals("attributeField")) + { + String attrName = attnElement.getAttribute("name"); + attributes.put(attrName, metaText); + } + else + { + metadata.put(metaName, metaText); + } + } + } +} diff --git a/lib/src/main/java/com/alphawallet/token/entity/TSFilterNode.java b/lib/src/main/java/com/alphawallet/token/entity/TSFilterNode.java index c72abf2ce3..b2511e366d 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/TSFilterNode.java +++ b/lib/src/main/java/com/alphawallet/token/entity/TSFilterNode.java @@ -99,6 +99,15 @@ LogicState evaluate(Map attrs) { String valueLeftStr = getValue(first, attrs); String valueRightStr = getValue(second, attrs); + boolean isBoolComparison = detemineBooleanComparison(valueLeftStr, valueRightStr); + String valueSupplementalLeftStr = valueLeftStr; + String valueSupplementalRightStr = valueRightStr; + + if (isBoolComparison) + { + valueSupplementalLeftStr = valueLeftStr.equalsIgnoreCase("true") ? "1" : valueLeftStr; + valueSupplementalRightStr = valueRightStr.equalsIgnoreCase("true") ? "1" : valueRightStr; + } BigInteger valueLeft = getBIValue(first, attrs); BigInteger valueRight = getBIValue(second, attrs); @@ -111,7 +120,8 @@ LogicState evaluate(Map attrs) { case EQUAL: //compare strings - return compareLogic(valueLeftStr.equalsIgnoreCase(valueRightStr)); + return compareLogic(valueLeftStr.equalsIgnoreCase(valueRightStr) + || (isBoolComparison && valueSupplementalLeftStr.equalsIgnoreCase(valueSupplementalRightStr))); case GREATER_THAN: //both sides must be values if (!bothSidesValues) return LogicState.FALSE; @@ -134,6 +144,14 @@ LogicState evaluate(Map attrs) } } + private boolean detemineBooleanComparison(String valueLeftStr, String valueRightStr) + { + return valueLeftStr.equalsIgnoreCase("true") + || valueLeftStr.equalsIgnoreCase("false") + || valueRightStr.equalsIgnoreCase("true") + || valueRightStr.equalsIgnoreCase("false"); + } + private LogicState compareLogic(boolean comparison) { if (comparison) diff --git a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java index a3f526d93c..0f0a687d9c 100644 --- a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java +++ b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java @@ -4,6 +4,7 @@ import com.alphawallet.token.entity.ActionModifier; import com.alphawallet.token.entity.As; +import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.ContractInfo; @@ -32,7 +33,6 @@ import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Bool; -import org.web3j.abi.datatypes.Bytes; import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Utf8String; @@ -48,7 +48,6 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -65,7 +64,7 @@ public class TokenDefinition protected Locale locale; public final Map contracts = new HashMap<>(); - public final Map attestations = new HashMap<>(); + public final Map attestations = new HashMap<>(); public final Map actions = new HashMap<>(); private Map labels = new HashMap<>(); // store plural etc for token name private final Map namedTypeLookup = new HashMap<>(); //used to protect against name collision @@ -129,7 +128,7 @@ public List getFunctionData() public Map getActivityCards() { return activityCards; } - public Attestation getAttestation() + public AttestationDefinition getAttestation() { return attestations.get(holdingToken); } @@ -478,7 +477,7 @@ private void extractTags(Element token) throws Exception } break; case "attestation": - Attestation attestation = scanAttestation(element); + AttestationDefinition attestation = scanAttestation(element); attestations.put(attestation.name, attestation); break; default: @@ -884,11 +883,11 @@ private void extractSignedInfo(Document xml) { return; // even if the document is signed, often it doesn't have KeyName } - private Attestation scanAttestation(Node attestationNode) throws SAXException + private AttestationDefinition scanAttestation(Node attestationNode) throws SAXException { Element element = (Element) attestationNode; String name = element.getAttribute("name"); - Attestation attn = new Attestation(name); + AttestationDefinition attn = new AttestationDefinition(name); for (Node n = attestationNode.getFirstChild(); n != null; n = n.getNextSibling()) { @@ -905,7 +904,20 @@ private Attestation scanAttestation(Node attestationNode) throws SAXException handleAttestationDisplay(attnElement); break; case "eas": - attn.addAttributes(attnElement); + ContractInfo info = attn.addAttributes(attnElement); + if (info != null) + { + contracts.put(attn.name, info); + } + break; + case "key": + attn.handleKey(attnElement); + break; + case "eventId": + attn.handleEventId(attnElement); + break; + case "idFields": + attn.handleReplacementField(attnElement); break; case "struct": case "ProofOfKnowledge": @@ -920,113 +932,16 @@ private Attestation scanAttestation(Node attestationNode) throws SAXException attn.function.as = parseAs(functionElement); break; } - - //String label = tokenType.getAttribute("label"); - /*switch (tokenType.getLocalName()) - { - case "token": - Element tokenSpec = getFirstChildElement(tokenType); - if (tokenSpec != null) - { - switch (tokenSpec.getLocalName()) - { - case "ethereum": - String chainIdStr = tokenSpec.getAttribute("network"); - long chainId = Long.parseLong(chainIdStr); - ContractInfo ci = new ContractInfo(tokenSpec.getLocalName()); - ci.addresses.put(chainId, new ArrayList<>(Arrays.asList(ci.contractInterface))); - contracts.put(label, ci); - break; - case "contract": - handleAddresses(getFirstChildElement(element)); - break; - default: - break; - } - } - break; - default: - break; - }*/ } return attn; } - /* - - - - - - - Devcon Referral Attestation - - - - - - - */ private void handleAttestationDisplay(Element attnElement) { } - - /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ - - //private final List attestation = new ArrayList<>(); - - /*private AttnElement handleAttestationStruct(Element attrElement) - { - for(Node n = attrElement.getFirstChild(); n!=null; n=n.getNextSibling()) - { - if (n.getNodeType() != ELEMENT_NODE) continue; - Element e = (Element) n; - - AttnElement attnE = parseAttestationStruct(e); - attestation.add(attnE); - } - }*/ - private List parseAttestationStructMembers(Node attnStruct) { //get struct list @@ -1068,10 +983,10 @@ private String getElementName(Node attribute) public AttestationValidation getValidation(List values) { //legacy attestations should only have one type - Attestation attn = null; + AttestationDefinition attn = null; if (attestations.size() > 0) { - attn = (Attestation)attestations.values().toArray()[0]; + attn = (AttestationDefinition)attestations.values().toArray()[0]; } if (attn == null || !namedTypeLookup.containsKey(attn.function.namedTypeReturn)) @@ -1121,10 +1036,10 @@ public AttestationValidation getValidation(List values) public List> getAttestationReturnTypes() { List> returnTypes = new ArrayList<>(); - Attestation attn = null; + AttestationDefinition attn = null; if (attestations.size() > 0) { - attn = (Attestation)attestations.values().toArray()[0]; + attn = (AttestationDefinition)attestations.values().toArray()[0]; } if (attn == null || !namedTypeLookup.containsKey(attn.function.namedTypeReturn)) @@ -1261,69 +1176,6 @@ private enum AttnStructType BOOL, } - public class Attestation - { - //public TSOrigins origin; //single value for validation - public FunctionDefinition function = null; - //public final List members; - public Map metadata; - public Map attributes; - public final String name; - public long chainId; - - public Attestation(String name) - { - this.name = name; - metadata = null; - attributes = null; - } - - public void addAttributes(Element element) - { - attributes = new HashMap<>(); - //get schemaUID attribute - String schemaUID = element.getAttribute("schemaUID"); - String networkStr = element.getAttribute("network"); - //this is the backlink to the attestation - ContractInfo info = new ContractInfo("Attestation"); - if (networkStr.length() > 0) - { - this.chainId = Long.parseLong(networkStr); - } - else - { - this.chainId = 1; - } - info.addresses.put(this.chainId, Collections.singletonList(schemaUID)); - contracts.put(this.name, info); - for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) - { - if (n.getNodeType() != ELEMENT_NODE) continue; - Element attnElement = (Element) n; - - String name = attnElement.getAttribute("name"); - String text = attnElement.getTextContent(); - - attributes.put(name, text); - } - } - - public void addMetaData(Element element) - { - metadata = new HashMap<>(); - for (Node n = element.getFirstChild(); n != null; n = n.getNextSibling()) - { - if (n.getNodeType() != ELEMENT_NODE) continue; - Element attnElement = (Element) n; - - String metaName = attnElement.getLocalName(); - String metaText = attnElement.getTextContent(); - - metadata.put(metaName, metaText); - } - } - } - private static class AttnElement { public String name; From e5f63b968551fd72edec42c0042d8e4c7591865f Mon Sep 17 00:00:00 2001 From: James Brown Date: Fri, 7 Jul 2023 21:30:26 +0300 Subject: [PATCH 10/11] Update tests --- app/src/test/java/com/alphawallet/app/ENSTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/alphawallet/app/ENSTest.java b/app/src/test/java/com/alphawallet/app/ENSTest.java index 5b7aba97db..bd88fff17c 100644 --- a/app/src/test/java/com/alphawallet/app/ENSTest.java +++ b/app/src/test/java/com/alphawallet/app/ENSTest.java @@ -68,7 +68,7 @@ public void testResolve() throws Exception { assertEquals( - ensResolver.resolve("web3j.eth"), ("0x7bfd522dea355ddee2be3c01dfa4419451759310").toLowerCase()); + ensResolver.resolve("web3j.eth"), ("0xd8a50a7ab452c0c9e5581bac5ff15558e6f671a1").toLowerCase()); assertEquals( ensResolver.resolve("1.offchainexample.eth"), ("0x41563129cdbbd0c5d3e1c86cf9563926b243834d").toLowerCase()); @@ -86,7 +86,7 @@ public void testResolve() throws Exception ensResolver.resolve("1.offchainexample.eth"), ("0x41563129cdbbd0c5d3e1c86cf9563926b243834d").toLowerCase()); assertEquals( - ensResolver.resolve("web3j.eth"), ("0x7bfd522dea355ddee2be3c01dfa4419451759310").toLowerCase()); + ensResolver.resolve("web3j.eth"), ("0xd8a50a7ab452c0c9e5581bac5ff15558e6f671a1").toLowerCase()); // assertEquals( // ensResolver.resolve("vladylav.wallet"), ("0xac1de5bbdc2c8d0b3e4324c87599dc66d3221c13").toLowerCase()); @@ -113,7 +113,7 @@ public void testAvatarResolve() throws Exception public void testAvatarAddressResolve() throws Exception { assertEquals( - ensResolver.resolveAvatarFromAddress("0xbc8dAfeacA658Ae0857C80D8Aa6dE4D487577c63"), ("eip155:1/erc721:0x222222222291749DE47895C0c0A9B17e4fcA8268/29")); + ensResolver.resolveAvatarFromAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"), ("eip155:1/erc1155:0xb32979486938aa9694bfc898f35dbed459f44424/10063")); } @Test From 05bc551d56266d4fd49d96a2c2d4a19e0b31b317 Mon Sep 17 00:00:00 2001 From: James Brown Date: Fri, 7 Jul 2023 22:42:08 +0300 Subject: [PATCH 11/11] tidy up source --- .../app/entity/tokens/Attestation.java | 2 +- .../app/ui/widget/adapter/TokensAdapter.java | 5 +++-- .../java/com/alphawallet/app/util/Utils.java | 22 +++++++++++++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index 4452d86d5b..a30b97e906 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -183,7 +183,7 @@ public String getAttestationCollectionId() //issuer public key //calculate hash from attestation - String hexStr = Numeric.cleanHexPrefix(easAttestation.schema).toLowerCase() + Keys.getAddress(recoverPublicKey(easAttestation)).toLowerCase() + String hexStr = Numeric.cleanHexPrefix(easAttestation.schema).toLowerCase(Locale.ROOT) + Keys.getAddress(recoverPublicKey(easAttestation)).toLowerCase(Locale.ROOT) + (!TextUtils.isEmpty(eventId) ? eventId : ""); //now convert this into ASCII hex bytes byte[] collectionBytes = hexStr.getBytes(StandardCharsets.UTF_8); diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java index eece644efa..2d19fb191a 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/TokensAdapter.java @@ -48,6 +48,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public class TokensAdapter extends RecyclerView.Adapter { @@ -393,7 +394,7 @@ public SortedItem removeToken(long chainId, String tokenAddress) public SortedItem removeAttestation(Token token) { Attestation attn = (Attestation)token; - String attnKey = attn.getDatabaseKey().toLowerCase(); + String attnKey = attn.getDatabaseKey().toLowerCase(Locale.ROOT); for (int i = 0; i < items.size(); i++) { Object si = items.get(i); @@ -402,7 +403,7 @@ public SortedItem removeAttestation(Token token) TokenSortedItem tsi = (TokenSortedItem) si; TokenCardMeta thisToken = tsi.value; - if (thisToken.tokenId.toLowerCase().startsWith(attnKey)) + if (thisToken.tokenId.toLowerCase(Locale.ROOT).startsWith(attnKey)) { return items.removeItemAt(i); } diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index dd06dceb32..241a546367 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -1143,12 +1143,26 @@ public static String getAttestationString(String url) { int hashIndex = url.indexOf("#attestation="); String decoded; - if (hashIndex >= 0) //EAS style attestations have the magic link style + try { - url = url.substring(hashIndex + 13); - decoded = URLDecoder.decode(url, StandardCharsets.UTF_8); + if (hashIndex >= 0) //EAS style attestations have the magic link style + { + url = url.substring(hashIndex + 13); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + { + decoded = URLDecoder.decode(url, StandardCharsets.UTF_8); + } + else + { + decoded = URLDecoder.decode(url, "UTF-8"); + } + } + else + { + decoded = url; + } } - else + catch (Exception e) { decoded = url; }