Skip to content

Commit

Permalink
feat: implementation of TS common viewContent, URL fragment & remote …
Browse files Browse the repository at this point in the history
…URL content (#2841)

* feat: initial implementation of TS viewContent & URL fragment.
- Add view content processing, to inject common view content
  when a view is loaded.
- Add URL hash/fragment location when loading the view.
* fix: getStyle instead of getTokenView
* fix: Add minimum TS schema version to support old versions
This allows adding new features to future schema versions
whilst maintaining backward compatibility with old schemas.

It adds a new static variable TOKENSCRIPT_MINIMUM_SCHEMA and
function isSchemaLessThanMinimum. This it then used to determine
if old definitions should be deleted. Old versions are now only
deleted if they don't meet the minimum, rather than not matching
the TOKENSCRIPT_CURRENT_SCHEMA.

* fix: namespace null check
* feat: TS - Add URL attribute functionality to load remote views

This change allows TokenScripts to use the url attribute of a view to
specify that the view should be loaded from a remote location such as IPFS.
For bigger applications, it becomes hard for the developer to embed all required
assets inside a small TokenScript; such as fonts, large graphics and other media.

This provides a different route that make it easier to make richer applications
while ensuring TokenScript TSML size remains small. It also demonstrates the
potential for existing DApps to be easily ported to TokenScript, provided
that Web3TokenView.java is updated to provide the full ethereum provider to the client.
Access to the full provider can be specified as a TokenScript view attribute,
adding an extra layer of security.
  • Loading branch information
micwallace authored Oct 3, 2023
1 parent 9b5641e commit 7a9d88c
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ private TokenDefinition fileLoadComplete(List<ContractLocator> originContracts,
try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB))
{
//check for out-of-date script in the secure (downloaded) zone
if (isInSecureZone(file) && !td.nameSpace.equals(TokenDefinition.TOKENSCRIPT_NAMESPACE))
if (isInSecureZone(file) && td.isSchemaLessThanMinimum())
{
//delete this file and check downloads for update
removeFile(file.getAbsolutePath());
Expand Down Expand Up @@ -1409,7 +1409,7 @@ private Single<File> fetchXMLFromServer(String address)
if (result != null && result.exists())
{
TokenDefinition td = getTokenDefinition(result);
if (definitionIsOutOfDate(td))
if (td.isSchemaLessThanMinimum())
{
removeFile(result.getAbsolutePath());
assetChecked.put(address, 0L);
Expand Down Expand Up @@ -1498,11 +1498,6 @@ private String[] getHeaders(long currentFileTime) throws PackageManager.NameNotF
};
}

private boolean definitionIsOutOfDate(TokenDefinition td)
{
return td != null && !td.nameSpace.equals(TokenDefinition.TOKENSCRIPT_NAMESPACE);
}

private void finishLoading()
{
assetLoadingLock.release();
Expand Down
18 changes: 12 additions & 6 deletions app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import dagger.hilt.android.AndroidEntryPoint;
import io.reactivex.android.schedulers.AndroidSchedulers;
Expand Down Expand Up @@ -166,12 +167,17 @@ private void displayFunction(String tokenAttrs)
TSAction action = functions.get(actionMethod);
String magicValues = viewModel.getAssetDefinitionService().getMagicValuesForInjection(token.tokenInfo.chainId);

String injectedView = tokenView.injectWeb3TokenInit(action.view.tokenView, tokenAttrs, tokenId);
injectedView = tokenView.injectJSAtEnd(injectedView, magicValues);
injectedView = tokenView.injectStyleAndWrapper(injectedView, action.style + "\n" + action.view.style);

String base64 = Base64.encodeToString(injectedView.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT);
tokenView.loadData(base64, "text/html; charset=utf-8", "base64");
if (Objects.equals(action.view.getUrl(), ""))
{
String injectedView = tokenView.injectWeb3TokenInit(action.view.getTokenView(), tokenAttrs, tokenId);
injectedView = tokenView.injectJSAtEnd(injectedView, magicValues);
injectedView = tokenView.injectStyleAndWrapper(injectedView, action.style + "\n" + action.view.getStyle());

String base64 = Base64.encodeToString(injectedView.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT);
tokenView.loadData(base64 + (!Objects.equals(action.view.getUrlFragment(), "") ? "#" + action.view.getUrlFragment() : ""), "text/html; charset=utf-8", "base64");
} else {
tokenView.loadUrl(action.view.getUrl());
}
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void bind(@Nullable EventMeta data, @NonNull Bundle addition)
if (td != null && td.getActivityCards().containsKey(eventData.getFunctionId()))
{
TSTokenView view = td.getActivityCards().get(eventData.getFunctionId()).getView(ASSET_SUMMARY_VIEW_NAME);
if (view != null) itemView = view.tokenView;
if (view != null) itemView = view.getTokenView();
}

String transactionValue = getEventAmount(eventData, transaction);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private void bindView(TokenTransferData data, Transaction tx)
if (td != null && td.getActivityCards().containsKey(data.eventName))
{
TSTokenView view = td.getActivityCards().get(data.eventName).getView(ASSET_SUMMARY_VIEW_NAME);
if (view != null) itemView = view.tokenView;
if (view != null) itemView = view.getTokenView();
}

String transactionValue = transferData.getEventAmount(token, tx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Locale;

import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_CURRENT_SCHEMA;
import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_MINIMUM_SCHEMA;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -550,8 +551,8 @@ public void testHoldingToken() {

@Test
public void entryTokenNameSpaceShouldBeCorrect() {
String currentNamespace = "http://tokenscript.org/" + TOKENSCRIPT_CURRENT_SCHEMA + "/tokenscript";
assertTrue("should match the latest namespace", entryToken.nameSpace.equals(currentNamespace));
String currentNamespace = "http://tokenscript.org/" + TOKENSCRIPT_MINIMUM_SCHEMA + "/tokenscript";
assertTrue("should match the minimum namespace", entryToken.nameSpace.equals(currentNamespace));
}

@Override
Expand All @@ -575,4 +576,4 @@ public void parseMessage(ParseResultId parseResult)
break;
}
}
}
}
64 changes: 59 additions & 5 deletions lib/src/main/java/com/alphawallet/token/entity/TSTokenView.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,37 @@
import static org.w3c.dom.Node.ELEMENT_NODE;
import static org.w3c.dom.Node.TEXT_NODE;

import com.alphawallet.token.tools.TokenDefinition;

import java.util.Objects;

/**
* Holds an individual Token View which consists of style and HTML view code
*
* Created by JB on 8/05/2020.
*/
public class TSTokenView
{
public final String tokenView;
public final String style;
private String tokenView = "";
private String style = "";

private Element element;
private TokenDefinition tokenDef;

public TSTokenView(Element element, TokenDefinition tokenDef)
{
this.element = element;
this.tokenDef = tokenDef;
}

public TSTokenView(Element element)
private void generateTokenView(Element element)
{

if (!Objects.equals(this.getUrl(), ""))
{
return;
}

String lStyle = "";
String lView = "";
for (int i = 0; i < element.getChildNodes().getLength(); i++)
Expand All @@ -34,6 +53,11 @@ public TSTokenView(Element element)
//record the style for this
lStyle += getHTMLContent(child);
break;
case "viewContent":
String name = child.getAttributes().getNamedItem("name").getTextContent();
Element content = this.tokenDef.getViewContent(name);
generateTokenView(content);
break;
default:
lView += getElementHTML(child);
break;
Expand All @@ -51,8 +75,38 @@ public TSTokenView(Element element)
}
}

tokenView = lView;
style = lStyle;
tokenView += lView;
style += lStyle;
}

public String getTokenView()
{
if (tokenView.isEmpty())
{
generateTokenView(this.element);
}

return tokenView;
}

public String getStyle()
{
if (style.isEmpty())
{
generateTokenView(this.element);
}

return style;
}

public String getUrl()
{
return this.element.getAttribute("url");
}

public String getUrlFragment()
{
return this.element.getAttribute("urlFragment");
}

public TSTokenView(String style, String view)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public String getView(String viewName)
TSTokenView v = views.get(viewName);
if (v != null)
{
return v.tokenView;
return v.getTokenView();
}
else
{
Expand All @@ -28,6 +28,6 @@ public String getView(String viewName)
public String getViewStyle(String viewName)
{
TSTokenView v = views.get(viewName);
return (globalStyle != null ? globalStyle : "") + (v != null ? v.style : "");
return (globalStyle != null ? globalStyle : "") + (v != null ? v.getStyle() : "");
}
}
51 changes: 45 additions & 6 deletions lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ public class TokenDefinition
private final TSTokenViewHolder tokenViews = new TSTokenViewHolder();
private final Map<String, TSSelection> selections = new HashMap<>();
private final Map<String, TSActivityView> activityCards = new HashMap<>();
private final Map<String, Element> viewContent = new HashMap<>();

public String nameSpace;
public TokenscriptContext context;
public String holdingToken = null;
private int actionCount;

public static final String TOKENSCRIPT_CURRENT_SCHEMA = "2020/06";
public static final String TOKENSCRIPT_MINIMUM_SCHEMA = "2020/06";
public static final String TOKENSCRIPT_CURRENT_SCHEMA = "2022/09";
public static final String TOKENSCRIPT_REPO_SERVER = "https://repo.tokenscript.org/";
public static final String TOKENSCRIPT_NAMESPACE = "http://tokenscript.org/" + TOKENSCRIPT_CURRENT_SCHEMA + "/tokenscript";

Expand Down Expand Up @@ -615,11 +617,19 @@ private void handleCards(Element cards) throws Exception
case "card":
extractCard(card);
break;
case "viewContent":
this.viewContent.put(card.getAttribute("name"), card);
break;
}
}
}
}

public Element getViewContent(String name)
{
return this.viewContent.get(name);
}

private TSActivityView processActivityView(Element card) throws Exception
{
NodeList ll = card.getChildNodes();
Expand All @@ -641,7 +651,7 @@ private TSActivityView processActivityView(Element card) throws Exception
case "view": //TODO: Localisation
case "item-view":
if (activityView == null) throw new SAXException("Activity card declared without origins tag");
activityView.addView(node.getLocalName(), new TSTokenView(element));
activityView.addView(node.getLocalName(), new TSTokenView(element, this));
break;
default:
throw new SAXException("Unknown tag <" + node.getLocalName() + "> tag in tokens");
Expand Down Expand Up @@ -670,7 +680,7 @@ private void processTokenCardElements(Element card) throws Exception
break;
case "view": //TODO: Localisation
case "item-view":
TSTokenView v = new TSTokenView(element);
TSTokenView v = new TSTokenView(element, this);
tokenViews.views.put(node.getLocalName(), v);
break;
case "view-iconified":
Expand Down Expand Up @@ -767,6 +777,35 @@ else if (thisDate.before(schemaDate))
}
}

public boolean isSchemaLessThanMinimum()
{

if (nameSpace == null)
{
return true;
}

int dateIndex = nameSpace.indexOf(TOKENSCRIPT_BASE_URL) + TOKENSCRIPT_BASE_URL.length();
int lastSeparator = nameSpace.lastIndexOf("/");
if ((lastSeparator - dateIndex) == 7)
{
try
{
DateFormat format = new SimpleDateFormat("yyyy/MM", Locale.ENGLISH);
Date thisDate = format.parse(nameSpace.substring(dateIndex, lastSeparator));
Date schemaDate = format.parse(TOKENSCRIPT_MINIMUM_SCHEMA);

return thisDate.before(schemaDate);
}
catch (Exception e)
{
return true;
}
}

return true;
}

private void extractCard(Element card) throws Exception
{
TSAction action;
Expand Down Expand Up @@ -848,7 +887,7 @@ private TSAction handleAction(Element action) throws Exception
case "selection":
throw new SAXException("<ts:selection> tag must be in main scope (eg same as <ts:origins>)");
case "view": //localised?
tsAction.view = new TSTokenView(element);
tsAction.view = new TSTokenView(element, this);
break;
case "style":
tsAction.style = getHTMLContent(element);
Expand Down Expand Up @@ -1686,8 +1725,8 @@ public String getCardData(String tag)
{
TSTokenView view = tokenViews.views.get("view");

if (tag.equals("view")) return view.tokenView;
else if (tag.equals("style")) return view.style;
if (tag.equals("view")) return view.getTokenView();
else if (tag.equals("style")) return view.getStyle();
else return null;
}

Expand Down

0 comments on commit 7a9d88c

Please sign in to comment.