diff --git a/lib/src/main/java/com/alphawallet/token/entity/AttributeType.java b/lib/src/main/java/com/alphawallet/token/entity/AttributeType.java index 7b2bd8380..7b4655ddf 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/AttributeType.java +++ b/lib/src/main/java/com/alphawallet/token/entity/AttributeType.java @@ -34,45 +34,22 @@ public class AttributeType { public Map members; private TokenDefinition definition; public FunctionDefinition function = null; + public EventDefinition event = null; public boolean userInput = false; public AttributeType(Element attr, TokenDefinition def) { definition = def; id = attr.getAttribute("id"); + name = id; //set name to id if not specified as = As.Unsigned; //default value - try { - switch (attr.getAttribute("syntax")) { // We don't validate syntax here; schema does it. - case "1.3.6.1.4.1.1466.115.121.1.6": - syntax = TokenDefinition.Syntax.BitString; - break; - case "1.3.6.1.4.1.1466.115.121.1.7": - syntax = TokenDefinition.Syntax.Boolean; - break; - case "1.3.6.1.4.1.1466.115.121.1.11": - syntax = TokenDefinition.Syntax.CountryString; - break; - case "1.3.6.1.4.1.1466.115.121.1.28": - syntax = TokenDefinition.Syntax.JPEG; - break; - case "1.3.6.1.4.1.1466.115.121.1.36": - syntax = TokenDefinition.Syntax.NumericString; - break; - case "1.3.6.1.4.1.1466.115.121.1.24": - syntax = TokenDefinition.Syntax.GeneralizedTime; - break; - case "1.3.6.1.4.1.1466.115.121.1.26": - syntax = TokenDefinition.Syntax.IA5String; - break; - case "1.3.6.1.4.1.1466.115.121.1.27": - syntax = TokenDefinition.Syntax.Integer; - break; - default: // unknown syntax treat as Directory String - syntax = TokenDefinition.Syntax.DirectoryString; - } - } catch (NullPointerException e) { // missing + + if(attr.getAttribute("syntax") != null) { + syntax = getSyntax(attr.getAttribute("syntax")); + } else { syntax = TokenDefinition.Syntax.DirectoryString; // 1.3.6.1.4.1.1466.115.121.1.15 } + for(Node node = attr.getFirstChild(); node!=null; node=node.getNextSibling()){ if (node.getNodeType() == Node.ELEMENT_NODE) { @@ -107,6 +84,30 @@ public class AttributeType { } } + private TokenDefinition.Syntax getSyntax(String ISO) { + switch (ISO) { + case "1.3.6.1.4.1.1466.115.121.1.6": + return TokenDefinition.Syntax.BitString; + case "1.3.6.1.4.1.1466.115.121.1.7": + return TokenDefinition.Syntax.Boolean; + case "1.3.6.1.4.1.1466.115.121.1.11": + return TokenDefinition.Syntax.CountryString; + case "1.3.6.1.4.1.1466.115.121.1.28": + return TokenDefinition.Syntax.JPEG; + case "1.3.6.1.4.1.1466.115.121.1.36": + return TokenDefinition.Syntax.NumericString; + case "1.3.6.1.4.1.1466.115.121.1.24": + return TokenDefinition.Syntax.GeneralizedTime; + case "1.3.6.1.4.1.1466.115.121.1.26": + return TokenDefinition.Syntax.IA5String; + case "1.3.6.1.4.1.1466.115.121.1.27": + return TokenDefinition.Syntax.Integer; + case "1.3.6.1.4.1.1466.115.121.1.15": + return TokenDefinition.Syntax.DirectoryString; + } + return null; + } + private void handleOrigins(Element origin) { for(Node node = origin.getFirstChild(); @@ -119,7 +120,15 @@ public class AttributeType { switch (node.getLocalName()) { case "ethereum": - function = definition.parseFunction(resolve, syntax); + if (resolve.hasAttribute("event")) + { + event = definition.parseEvent(resolve, syntax); + event.attributeId = id; + } + else if (resolve.hasAttribute("function")) + { + function = definition.parseFunction(resolve, syntax); + } //drop through (no break) case "token-id": //this value is obtained from the token id @@ -234,6 +243,40 @@ public class AttributeType { } } + //Sometimes value needs to be processed from the raw input. + //Currently only time + public BigInteger processValue(BigInteger val) + { + switch (syntax) + { + case GeneralizedTime: + return parseGeneralizedTime(val); + case DirectoryString: + case IA5String: + case Integer: + case Boolean: + case BitString: + case CountryString: + case JPEG: + case NumericString: + break; + } + return val; + } + + private BigInteger parseGeneralizedTime(BigInteger value) { + try + { + DateTime dt = DateTimeFactory.getDateTime(toString(value)); + return BigInteger.valueOf(dt.toEpoch()); + } + catch (ParseException|UnsupportedEncodingException p) + { + p.printStackTrace(); + return value; + } + } + private String checkAlphaNum(String data) { for (char ch : data.toCharArray()) diff --git a/lib/src/main/java/com/alphawallet/token/entity/ContractInfo.java b/lib/src/main/java/com/alphawallet/token/entity/ContractInfo.java index ef9b03140..c26382e33 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/ContractInfo.java +++ b/lib/src/main/java/com/alphawallet/token/entity/ContractInfo.java @@ -12,4 +12,16 @@ public class ContractInfo { public String contractInterface = null; public Map> addresses = new HashMap<>(); + public Map eventModules = null; + + public boolean hasContractTokenScript(int chainId, String address) + { + if (addresses == null) + { + return false; + } else { + List addrs = addresses.get(chainId); + return addrs != null && addrs.contains(address); + } + } } diff --git a/lib/src/main/java/com/alphawallet/token/entity/EventDefinition.java b/lib/src/main/java/com/alphawallet/token/entity/EventDefinition.java new file mode 100644 index 000000000..06b12cdd6 --- /dev/null +++ b/lib/src/main/java/com/alphawallet/token/entity/EventDefinition.java @@ -0,0 +1,60 @@ +package com.alphawallet.token.entity; + +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Created by JB on 21/03/2020. + */ +public class EventDefinition +{ + public ContractInfo originContract; + public String attributeId; //TransactionResult: method + public String eventName; + public Module eventModule; + public String filter; + public String select; + public BigInteger readBlock; + public boolean hasNewEvent = false; + + public String getFilterTopicValue() + { + // (\+\d{4}|\-\d{4}) + Matcher m = Pattern.compile("\\$\\{([^}]+)\\}").matcher(filter); + String item = m.find() ? m.group(1) : null; + return item; + } + + public String getFilterTopicIndex() + { + String[] item = filter.split("="); + return item[0]; + } + + public int getTopicIndex(String filterTopic) + { + if (eventModule == null || filterTopic == null) return -1; + return eventModule.getTopicIndex(filterTopic); + } + + public int getSelectIndex(boolean indexed) + { + int index = 0; + boolean found = false; + for (String label : eventModule.getArgNames(indexed)) + { + if (label.equals(select)) + { + found = true; + break; + } + else + { + index++; + } + } + + return found ? index : -1; + } +} diff --git a/lib/src/main/java/com/alphawallet/token/entity/Module.java b/lib/src/main/java/com/alphawallet/token/entity/Module.java new file mode 100644 index 000000000..beb87d237 --- /dev/null +++ b/lib/src/main/java/com/alphawallet/token/entity/Module.java @@ -0,0 +1,89 @@ +package com.alphawallet.token.entity; + +import org.w3c.dom.Element; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by JB on 20/03/2020. + */ +public class Module +{ + public final ContractInfo contractInfo; + public List sequence = new ArrayList<>(); + + public Module(ContractInfo info) + { + contractInfo = info; + } + + public void addSequenceElement(Element element, String sequenceName) throws Exception + { + SequenceElement se = new SequenceElement(); + String indexed = element.getAttribute("ethereum:indexed"); + se.indexed = indexed != null && indexed.equalsIgnoreCase("true"); + se.type = element.getAttribute("ethereum:type"); + se.name = element.getAttribute("name"); + sequence.add(se); + + if (se.type == null) + { + throw new Exception("Malformed sequence element in: " + sequenceName + " name: " + se.name); + } + else if (se.name == null) + { + throw new Exception("Malformed sequence element in: " + sequenceName + " type: " + se.type); + } + } + + + public List getSequenceArgs() + { + return sequence; + } + + public List getArgNames(boolean indexed) + { + List argNameIndexedList = new ArrayList<>(); + for (SequenceElement se : sequence) + { + if (se.indexed == indexed) + { + argNameIndexedList.add(se.name); + } + } + + return argNameIndexedList; + } + + int getTopicIndex(String filterTopic) + { + int topicIndex = -1; + int currentIndex = 0; + for (SequenceElement se : sequence) + { + if (se.indexed) + { + if (se.name.equals(filterTopic)) + { + topicIndex = currentIndex; + break; + } + else + { + currentIndex++; + } + } + } + + return topicIndex; + } + + public class SequenceElement + { + public String name; + public String type; + public boolean indexed; + } +} 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 899d12b17..517de994f 100644 --- a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java +++ b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java @@ -3,6 +3,7 @@ package com.alphawallet.token.tools; import com.alphawallet.token.entity.*; import org.w3c.dom.*; import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -26,6 +27,7 @@ public class TokenDefinition { public Map> attributeSets = new HashMap<>(); //TODO: add language, in case user changes language during operation - see Weiwu's comment further down public Map actions = new HashMap<>(); private Map names = new HashMap<>(); // store plural etc for token name + private Map moduleLookup = null; //used to protect against name collision public String nameSpace; public TokenscriptContext context; @@ -76,10 +78,35 @@ public class TokenDefinition { return defs; } + public EventDefinition parseEvent(Element resolve, Syntax syntax) + { + EventDefinition ev = new EventDefinition(); + + for (int i = 0; i < resolve.getAttributes().getLength(); i++) + { + Node thisAttr = resolve.getAttributes().item(i); + String attrValue = thisAttr.getNodeValue(); + switch (thisAttr.getNodeName()) + { + case "event": + ev.eventName = attrValue; + ev.eventModule = moduleLookup.get(attrValue); + break; + case "filter": + ev.filter = attrValue; + break; + case "select": + ev.select = attrValue; + break; + } + } + + return ev; + } + public FunctionDefinition parseFunction(Element resolve, Syntax syntax) { FunctionDefinition function = new FunctionDefinition(); - //this value is obtained from a contract call String contract = resolve.getAttribute("contract"); function.contract = contracts.get(contract); function.method = resolve.getAttribute("function"); @@ -673,7 +700,7 @@ public class TokenDefinition { } } - private void parseOrigins(Element origins) + private void parseOrigins(Element origins) throws SAXParseException { for (Node n = origins.getFirstChild(); n != null; n = n.getNextSibling()) { @@ -693,7 +720,7 @@ public class TokenDefinition { } } - private void handleAddresses(Element contract) + private void handleAddresses(Element contract) throws Exception { NodeList nList = contract.getElementsByTagNameNS(nameSpace, "address"); ContractInfo info = new ContractInfo(); @@ -701,31 +728,90 @@ public class TokenDefinition { info.contractInterface = contract.getAttribute("interface"); contracts.put(name, info); - for (int addrIndex = 0; addrIndex < nList.getLength(); addrIndex++) + for (Node n = contract.getFirstChild(); n != null; n = n.getNextSibling()) { - Node node = nList.item(addrIndex); - if (node.getNodeType() == ELEMENT_NODE) + if (n.getNodeType() == ELEMENT_NODE) { - Element addressElement = (Element) node; - String networkStr = addressElement.getAttribute("network"); - int network = 1; - if (networkStr != null) network = Integer.parseInt(networkStr); - String address = addressElement.getTextContent().toLowerCase(); - List addresses = info.addresses.get(network); - if (addresses == null) + Element element = (Element) n; + switch (element.getLocalName()) { - addresses = new ArrayList<>(); - info.addresses.put(network, addresses); + case "address": + handleAddress(element, info); + break; + case "module": + handleModule(element, info); + break; } + } + } + } + + private void handleModule(Element module, ContractInfo info) throws Exception + { + String moduleName = module.getAttribute("name"); + if (moduleName == null) throw new Exception("Module requires name"); + if (moduleLookup == null) + { + moduleLookup = new HashMap<>(); + } + else if (moduleLookup.containsKey(moduleName)) + { + throw new Exception("Duplicate Module name: " + moduleName); + } - if (!addresses.contains(address)) + for (Node n = module.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() == ELEMENT_NODE) + { + switch (n.getNodeName()) { - addresses.add(address); + case "sequence": + Module eventModule = handleElementSequence((Element)n, info, moduleName); + if (info.eventModules == null) info.eventModules = new HashMap<>(); + info.eventModules.put(moduleName, eventModule); + moduleLookup.put(moduleName, eventModule); + break; + default: + break; } } } } + private Module handleElementSequence(Element sequence, ContractInfo info, String moduleName) throws Exception + { + Module module = new Module(info); + for (Node n = sequence.getFirstChild(); n != null; n = n.getNextSibling()) + { + if (n.getNodeType() == ELEMENT_NODE) + { + Element element = (Element)n; + module.addSequenceElement(element, moduleName); + } + } + + return module; + } + + private void handleAddress(Element addressElement, ContractInfo info) + { + String networkStr = addressElement.getAttribute("network"); + int network = 1; + if (networkStr != null) network = Integer.parseInt(networkStr); + String address = addressElement.getTextContent().toLowerCase(); + List addresses = info.addresses.get(network); + if (addresses == null) + { + addresses = new ArrayList<>(); + info.addresses.put(network, addresses); + } + + if (!addresses.contains(address)) + { + addresses.add(address); + } + } + private String getHTMLContent(Node content) { StringBuilder sb = new StringBuilder();