Repository: alphawallet/alpha-wallet-android
Issue: TokenScripts don't update after conditional on props

Example:

Compound requires you enable the dai contract before using supply or withdraw, our TokenScript checks if you have enabled and if so, allows you to supply/withdraw and if not displays a message saying you must enable first.

In Android this does not work, when you have updated, the prop check fails to update the screen to allow you to supply/withdraw after checking the allowance prop to see if it is enabled. 

Changed files:
app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenscriptFunction.java
app/src/main/java/com/alphawallet/app/viewmodel/TransactionsViewModel.java
app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java

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 1878d2ac4..13f4567e0 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
@@ -563,6 +563,29 @@ public abstract class TokenscriptFunction
         }
     }
 
+    /**
+     * Fetches a TokenScript attribute
+     *
+     * May either return a static attribute sourced from the Token ID or a dynamic one from a contract function
+     *
+     * If a dynamic function, then test to see if we can source the value from the result cache -
+     * Test to see if the contract has seen any transactions AFTER the result was cached, if so then invalidate the result and re-fetch
+     *
+     * TODO: there is currently an optimisation issue with this. If the contract being called to resolve the attribute is not the token
+     *       contract which the script refers to then the result is not cached. This can be seen eg with 'ENABLE' functions which permit
+     *       the script contract to manipulate the tokens which the 'ENABLE' function is being called on.
+     *       It may not be possible to always safely cache these values; even with an event handler we have to interpret those events and invalidate
+     *       any cached results. However if we're tracking the referenced contract as a token then it should be safe
+     *
+     * @param walletAddress
+     * @param attribute
+     * @param tokenId
+     * @param cAddr
+     * @param td
+     * @param attrIf
+     * @param transactionUpdate
+     * @return
+     */
     public Observable<TokenScriptResult.Attribute> fetchAttrResult(String walletAddress, String attribute, BigInteger tokenId, ContractAddress cAddr, TokenDefinition td, AttributeInterface attrIf, long transactionUpdate)
     {
         AttributeType attr = td.attributeTypes.get(attribute);
@@ -576,17 +599,17 @@ public abstract class TokenscriptFunction
             ContractAddress useAddress;
             if (cAddr == null) useAddress = new ContractAddress(attr.function);
             else useAddress = new ContractAddress(attr.function, cAddr.chainId, cAddr.address);
-            TransactionResult transactionResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds
-            if (attrIf.resolveOptimisedAttr(useAddress, attr, transactionResult) || !transactionResult.needsUpdating(transactionUpdate)) //can we use wallet's known data or cached value?
+            TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds
+            if (cAddr != null && !useAddress.address.equalsIgnoreCase(cAddr.address)) transactionUpdate = 0; //If calling a function which isn't the main tokenscript function retrieve from contract call not cache
+            if (attrIf.resolveOptimisedAttr(useAddress, attr, cachedResult) || !cachedResult.needsUpdating(transactionUpdate)) //can we use wallet's known data or cached value?
             {
-                return resultFromDatabase(transactionResult, attr);
+                return resultFromDatabase(cachedResult, attr);
             }
-            else  //if value is old or there wasn't any previous value
+            else  //if cached value is invalid or if value is dynamic
             {
-                //for function query, never need wallet address
                 return fetchResultFromEthereum(walletAddress, useAddress, attr, tokenId, td, attrIf, transactionUpdate)       // Fetch function result from blockchain
-                        .map(result -> restoreFromDBIfRequired(result, transactionResult))  // If network unavailable restore value from cache
-                        .map(attrIf::storeAuxData)                                          // store new data
+                        .map(result -> restoreFromDBIfRequired(result, cachedResult))  // If network unavailable restore value from cache
+                        .map(attrIf::storeAuxData)                                     // store new data
                         .map(result -> parseFunctionResult(result, attr));    // write returned data into attribute
             }
         }
@@ -640,16 +663,16 @@ public abstract class TokenscriptFunction
 
     /**
      * Restore result from Database if required (eg connection failure), and if there was a database value to restore
-     * @param result
-     * @param transactionResult
+     * @param result return from calling contract function
+     * @param cachedResult previous return value, restored from database
      * @return
      */
-    private TransactionResult restoreFromDBIfRequired(TransactionResult result, TransactionResult transactionResult)
+    private TransactionResult restoreFromDBIfRequired(TransactionResult result, TransactionResult cachedResult)
     {
-        if (result.resultTime == 0 && transactionResult != null)
+        if (result.resultTime == 0 && cachedResult != null && result.result == null)
         {
-            result.result = transactionResult.result;
-            result.resultTime = transactionResult.resultTime;
+            result.result = cachedResult.result;
+            result.resultTime = cachedResult.resultTime;
         }
 
         return result;
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 a939a6533..e3ea9e263 100644
--- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java
+++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java
@@ -1122,7 +1122,6 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface
                     @Override
                     public void onStart()
                     {
-                        if (tResult.result == null) tResult.result = "";
                         if (!WalletUtils.isValidAddress(tokensService.getCurrentAddress())) return;
                         realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress());
                         ContractAddress cAddr = new ContractAddress(tResult.contractChainId, tResult.contractAddress);
@@ -1137,7 +1136,7 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface
                             realm.beginTransaction();
                             createAuxData(realm, tResult);
                         }
-                        else if (realmToken.getResultTime() != tResult.resultTime)
+                        else if (tResult.result != null)
                         {
                             TransactionsRealmCache.addRealm();
                             realm.beginTransaction();
diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionsViewModel.java
index 932b6a67a..2cc63b199 100644
--- a/app/src/main/java/com/alphawallet/app/viewmodel/TransactionsViewModel.java
+++ b/app/src/main/java/com/alphawallet/app/viewmodel/TransactionsViewModel.java
@@ -146,11 +146,11 @@ public class TransactionsViewModel extends BaseViewModel
 
     private void checkUnknownTokens()
     {
-        if (queryUnknownTokensDisposable == null && !unknownTokens.isEmpty())
+        if (queryUnknownTokensDisposable == null)
         {
             UnknownToken t = unknownTokens.poll();
 
-            if (tokensService.getToken(t.chainId, t.address) == null)
+            if (t != null && tokensService.getToken(t.chainId, t.address) == null)
             {
                 queryUnknownTokensDisposable = setupTokensInteract.addToken(t.address, t.chainId) //fetch tokenInfo
                         .filter(tokenInfo -> tokenInfo.name != null)
@@ -255,6 +255,8 @@ public class TransactionsViewModel extends BaseViewModel
             if (!txMap.containsKey(tx.hash))
             {
                 txMap.put(tx.hash, tx);
+                //does this transaction correspond to a known token?
+                checkTokenTransactions(tx);
             }
         }
 
@@ -262,6 +264,18 @@ public class TransactionsViewModel extends BaseViewModel
         fetchTransactionDisposable = null;
     }
 
+    private void checkTokenTransactions(Transaction tx)
+    {
+        Token t = tokensService.getToken(tx.chainId, tx.to);
+        Long blockNumber = Long.parseLong(tx.blockNumber);
+        if (t != null && !t.isEthereum() && t.lastBlockCheck < blockNumber)
+        {
+            t.lastBlockCheck = blockNumber;
+            t.lastTxTime = tx.timeStamp*1000; //update last transaction received time
+            addTokenInteract.updateBlockRead(t, defaultWallet().getValue());
+        }
+    }
+
     private void onUpdateTransactions(Transaction[] transactions, Token token)
     {
         fetchTransactionDisposable = null;