package com.alphawallet.app.service;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Environment;
import android.os.FileObserver;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.SparseArray;

import com.alphawallet.app.BuildConfig;
import com.alphawallet.app.C;
import com.alphawallet.app.entity.ActionEventCallback;
import com.alphawallet.app.entity.ContractLocator;
import com.alphawallet.app.entity.ContractType;
import com.alphawallet.app.entity.Event;
import com.alphawallet.app.entity.FragmentMessenger;
import com.alphawallet.app.entity.TokenLocator;
import com.alphawallet.app.entity.Wallet;
import com.alphawallet.app.entity.opensea.Asset;
import com.alphawallet.app.entity.tokens.ERC721Token;
import com.alphawallet.app.entity.tokens.Token;
import com.alphawallet.app.entity.tokens.TokenFactory;
import com.alphawallet.app.entity.tokenscript.EventUtils;
import com.alphawallet.app.entity.tokenscript.TokenScriptFile;
import com.alphawallet.app.entity.tokenscript.TokenscriptFunction;
import com.alphawallet.app.repository.EthereumNetworkRepositoryType;
import com.alphawallet.app.repository.TokenLocalSource;
import com.alphawallet.app.repository.TransactionsRealmCache;
import com.alphawallet.app.repository.entity.RealmAuxData;
import com.alphawallet.app.repository.entity.RealmCertificateData;
import com.alphawallet.app.ui.HomeActivity;
import com.alphawallet.app.util.Utils;
import com.alphawallet.app.viewmodel.HomeViewModel;
import com.alphawallet.token.entity.Attribute;
import com.alphawallet.token.entity.AttributeInterface;
import com.alphawallet.token.entity.ContractAddress;
import com.alphawallet.token.entity.ContractInfo;
import com.alphawallet.token.entity.EvaluateSelection;
import com.alphawallet.token.entity.EventDefinition;
import com.alphawallet.token.entity.FunctionDefinition;
import com.alphawallet.token.entity.MethodArg;
import com.alphawallet.token.entity.ParseResult;
import com.alphawallet.token.entity.SigReturnType;
import com.alphawallet.token.entity.TSAction;
import com.alphawallet.token.entity.TSSelection;
import com.alphawallet.token.entity.TokenScriptResult;
import com.alphawallet.token.entity.TokenscriptContext;
import com.alphawallet.token.entity.TokenscriptElement;
import com.alphawallet.token.entity.TransactionResult;
import com.alphawallet.token.entity.XMLDsigDescriptor;
import com.alphawallet.token.tools.Numeric;
import com.alphawallet.token.tools.TokenDefinition;

import org.jetbrains.annotations.NotNull;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.datatypes.Function;
import org.web3j.crypto.WalletUtils;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.request.EthFilter;
import org.web3j.protocol.core.methods.response.EthBlock;
import org.web3j.protocol.core.methods.response.EthLog;
import org.web3j.protocol.core.methods.response.Log;
import org.xml.sax.SAXException;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.schedulers.Schedulers;
import io.realm.Realm;
import io.realm.RealmResults;
import io.realm.Sort;
import io.realm.exceptions.RealmPrimaryKeyConstraintException;
import okhttp3.OkHttpClient;
import okhttp3.Request;

import static com.alphawallet.app.C.ADDED_TOKEN;
import static com.alphawallet.app.repository.TokenRepository.getWeb3jService;
import static com.alphawallet.app.repository.TokensRealmSource.IMAGES_DB;
import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_CURRENT_SCHEMA;
import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_REPO_SERVER;


/**
 * AssetDefinitionService is the only place where we interface with XML files.
 * This is to avoid duplicating resources
 * and also provide a consistent way to get XML values
 */

public class AssetDefinitionService implements ParseResult, AttributeInterface
{
    public static final String ASSET_SUMMARY_VIEW_NAME = "item-view";
    public static final String ASSET_DETAIL_VIEW_NAME = "view";

    private static final String CERTIFICATE_DB = "CERTIFICATE_CACHE-db.realm";
    private static final long CHECK_TX_LOGS_INTERVAL = 15;
    private final Context context;
    private final OkHttpClient okHttpClient;

    private final SparseArray<Map<String, TokenScriptFile>> assetDefinitions;
    private final Map<String, Long> assetChecked;                //Mapping of contract address to when they were last fetched from server
    private FileObserver fileObserver;                     //Observer which scans the override directory waiting for file change
    private FileObserver fileObserverQ;                    //Observer for Android Q directory
    private final NotificationService notificationService;
    private final RealmManager realmManager;
    private final EthereumNetworkRepositoryType ethereumNetworkRepository;
    private final TokensService tokensService;
    private final TokenLocalSource tokenLocalSource;
    private final AlphaWalletService alphaWalletService;
    private TokenDefinition cachedDefinition = null;
    private final SparseArray<Map<String, SparseArray<String>>> tokenTypeName;
    private final List<EventDefinition> eventList = new ArrayList<>(); //List of events built during file load
    private final Semaphore assetLoadingLock;  // used to block if someone calls getAssetDefinitionASync() while loading
    private Disposable eventListener;           // timer thread that periodically checks event logs for scripts that require events
    private ActionEventCallback eventCallback;
    private boolean requireEventSend = false;
    private final Semaphore eventConnection;
    private FragmentMessenger homeMessenger;

    private final TokenscriptFunction tokenscriptUtility;
    private final EventUtils eventUtils;

    /* Designed with the assmuption that only a single instance of this class at any given time
    *  ^^ The "service" part of AssetDefinitionService is the keyword here.
    *  This is shorthand in the project to indicate this is a singleton that other classes inject.
    *  This is the design pattern of the app. See class RepositoriesModule for constructors which are called at App init only */
    public AssetDefinitionService(OkHttpClient client, Context ctx, NotificationService svs,
                                  RealmManager rm, EthereumNetworkRepositoryType eth, TokensService tokensService,
                                  TokenLocalSource trs, AlphaWalletService alphaService)
    {
        context = ctx;
        okHttpClient = client;
        assetChecked = new ConcurrentHashMap<>();
        tokenTypeName = new SparseArray<>();
        assetDefinitions = new SparseArray<>();
        notificationService = svs;
        realmManager = rm;
        ethereumNetworkRepository = eth;
        alphaWalletService = alphaService;
        this.tokensService = tokensService;
        this.eventUtils = new EventUtils() { }; //no overridden functions
        tokenscriptUtility = new TokenscriptFunction() { }; //no overridden functions
        tokenLocalSource = trs;
        assetLoadingLock = new Semaphore(1);
        eventConnection = new Semaphore(1);
        loadAssetScripts();
    }

    /**
     * Load all TokenScripts
     *
     * This order has to be observed because it's an expected developer override order. If a script is placed in the /AlphaWallet directory
     * it is expected to override the one fetched from the repo server.
     * If a developer clicks on a script intent this script is expected to override the one fetched from the server.
     */
    private void loadAssetScripts()
    {
        try
        {
            assetLoadingLock.acquire(); // acquire the semaphore here to prevent attributes from being fetched until loading is complete
            // See flow above for details
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }

        loadInternalAssets();
    }

    //This loads bundled TokenScripts in the /assets directory eg xDAI bridge
    private void loadInternalAssets()
    {
        assetDefinitions.clear();

        Observable.fromIterable(getLocalTSMLFiles())
                .subscribeOn(Schedulers.io())
                .subscribe(this::addContractAssets, error -> { onError(error); parseAllFileScripts(); },
                        this::parseAllFileScripts).isDisposed();
    }

    private void parseAllFileScripts()
    {
        final File[] files = buildFileList(); //build an ordered list of files that need parsing
        //1. Signed files downloaded from server.
        //2. Files placed in the Android OS external directory (Android/data/<App Package Name>/files)
        //3. Files placed in the /AlphaWallet directory.
        //Depending on the order placed, files can be overridden. A file downloaded from the server is
        //overridden by a script for the same token placed in the /AlphaWallet directory.

        Observable.fromArray(files)
                .filter(File::isFile)
                .filter(this::allowableExtension)
                .filter(File::canRead)
                .subscribeOn(Schedulers.io())
                .observeOn(Schedulers.io())
                .blockingForEach(file -> {  //load sequentially
                    try
                    {
                        final TokenDefinition td = parseFile(new FileInputStream(file));
                        cacheSignature(file)
                                .map(definition -> addContractAddresses(td, file))
                                .subscribe(success -> fileLoadComplete(success, file, td),
                                           error -> handleFileLoadError(error, file))
                                .isDisposed();
                    }
                    catch (Exception e)
                    {
                        handleFileLoadError(e, file);
                    }
                } );

        //executes after observable completes due to blockingForEach
        startDirectoryListeners();
        finishLoading();
    }

    private void handleFileLoadError(Throwable throwable, File file)
    {
        //TODO: parse error and add to error list for Token Management page
        System.out.println("ERROR WHILE PARSING: " + file.getName() + " : " + throwable.getMessage());
    }

    private void fileLoadComplete(List<ContractLocator> originContracts, File file, TokenDefinition td)
    {
        if (originContracts.size() == 0)
        {
            //TODO: parse error and add to error list for Token Management page
            System.out.println("File: " + file.getName() + " has no origin token");
        }
        else
        {
            //check for out-of-date script in the secure (downloaded) zone
            if (isInSecureZone(file) && !td.nameSpace.equals(TokenDefinition.TOKENSCRIPT_NAMESPACE))
            {
                //delete this file and check downloads for update
                removeFile(file.getAbsolutePath());
                loadScriptFromServer(getFileName(file));
            }
        }
    }

    //Start listening to the two script directories for files dropped in.
    //Why two directories? User may not want to allow AlphaWallet to have read file permission,
    //but we still want to allow them to be able to click on scripts in eg Telegram and install them
    //without needing to go through a permission screen
    //Using AlphaWallet directory is more convenient for developers using eg Android Studio to drop files into their phone
    private void startDirectoryListeners()
    {
        //listen for new files dropped into app external directory
        fileObserverQ = startFileListener(context.getExternalFilesDir("").getAbsolutePath());
        startAlphaWalletListener();
    }

    public void startAlphaWalletListener()
    {
        //listen for new files dropped into AlphaWallet directory, if we have permission
        if (checkReadPermission())
        {
            File alphaWalletDir = new File(
                    Environment.getExternalStorageDirectory()
                            + File.separator + HomeViewModel.ALPHAWALLET_DIR);

            if (alphaWalletDir.exists())
            {
                fileObserver = startFileListener(alphaWalletDir.getAbsolutePath());
            }
        }
    }

    public void onDestroy()
    {
        if (fileObserver != null) fileObserver.stopWatching();
        if (fileObserverQ != null) fileObserverQ.stopWatching();
        if (eventListener != null && !eventListener.isDisposed()) eventListener.dispose();
    }

    private File[] buildFileList()
    {
        List<File> fileList = new ArrayList<>();
        try
        {
            File[] files = context.getFilesDir().listFiles();
            if (files != null) fileList.addAll(Arrays.asList(files)); //first add files in app internal area - these are downloaded from the server
            files = context.getExternalFilesDir("").listFiles();
            if (files != null) fileList.addAll(Arrays.asList(files)); //now add files in the app's external directory; /Android/data/[app-name]/files. These override internal

            if (checkReadPermission())
            {
                File alphaWalletDir = new File(
                        Environment.getExternalStorageDirectory()
                                + File.separator + HomeViewModel.ALPHAWALLET_DIR);

                if (alphaWalletDir.exists())
                {
                    files = alphaWalletDir.listFiles();
                    if (files != null) fileList.addAll(Arrays.asList(files));
                }
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        if (fileList.size() == 0) finishLoading();

        return fileList.toArray(new File[0]);
    }

    @Override
    public boolean resolveOptimisedAttr(ContractAddress contract, Attribute attr, TransactionResult transactionResult)
    {
        boolean optimised = false;
        if (attr.function == null) return false;
        Token checkToken = tokensService.getToken(contract.chainId, contract.address);
        if (attr.function.method.equals("balanceOf") && checkToken != null)
        {
            //ensure the arg check for this function call is checking the correct balance address
            for (MethodArg arg : attr.function.parameters)
            {
                if (arg.parameterType.equals("address") && arg.element.ref.equals("ownerAddress"))
                {
                    transactionResult.result = checkToken.balance.toString();
                    transactionResult.resultTime = checkToken.updateBlancaTime;
                    optimised = true;
                    break;
                }
            }
        }

        return optimised;
    }

    @Override
    public String getWalletAddr()
    {
        return tokensService.getCurrentAddress();
    }

    @Override
    public long getLastTokenUpdate(int chainId, String address)
    {
        long txUpdateTime = 0;
        Token token = tokensService.getToken(chainId, address);
        if (token != null)
        {
            txUpdateTime = token.lastTxUpdate;
        }

        return txUpdateTime;
    };

    @Override
    public Attribute fetchAttribute(ContractInfo origin, String attributeName)
    {
        String addr = null;
        TokenDefinition td = null;
        int chainId = origin.addresses.keySet().iterator().next();
        if (origin.addresses.get(chainId).size() > 0) addr = origin.addresses.get(chainId).get(0);
        if (addr != null) td = getAssetDefinition(chainId, addr);
        if (td != null)
        {
            return td.attributes.get(attributeName);
        }
        else
        {
            return null;
        }
    }

    @Override
    public TokenScriptResult.Attribute fetchAttrResult(ContractAddress origin, Attribute attr, BigInteger tokenId)
    {
        TokenDefinition td = getAssetDefinition(origin.chainId, origin.address);
        Token originToken = tokensService.getToken(origin.chainId, origin.address);
        if (originToken == null || td == null) return null;

        //produce result
        return tokenscriptUtility.fetchAttrResult(originToken, attr, tokenId, td, this, false).blockingSingle();
    }

    public void addLocalRefs(Map<String, String> refs)
    {
        tokenscriptUtility.addLocalRefs(refs);
    }

    private Attribute getTypeFromList(String key, List<Attribute> attrList)
    {
        for (Attribute attr : attrList)
        {
            if (attr.name.equals(key)) return attr;
        }

        return null;
    }

    /**
     * Fetch attributes from local storage; not using contract lookup
     *
     * @param token
     * @param tokenId
     * @return
     */
    public TokenScriptResult getTokenScriptResult(Token token, BigInteger tokenId)
    {
        TokenScriptResult result = new TokenScriptResult();
        TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        if (definition != null)
        {
            for (String key : definition.attributes.keySet())
            {
                result.setAttribute(key, getTokenscriptAttr(definition, tokenId, key));
            }
        }

        return result;
    }

    private TokenScriptResult.Attribute getTokenscriptAttr(TokenDefinition td, BigInteger tokenId, String attribute)
    {
        TokenScriptResult.Attribute result = null;
        Attribute attrtype = td.attributes.get(attribute);
        try
        {
            if (attrtype == null)
            {
                return null;
            }
            else if (attrtype.event != null)
            {
                result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, tokenId, "unsupported encoding");
            }
            else if (attrtype.function != null)
            {
                //should be sourced from function
                ContractAddress cAddr = new ContractAddress(attrtype.function);
                TransactionResult tResult = getFunctionResult(cAddr, attrtype, tokenId); //t.getTokenIdResults(BigInteger.ZERO);
                result = tokenscriptUtility.parseFunctionResult(tResult, attrtype);//  attrtype.function.parseFunctionResult(tResult, attrtype);
            }
            else
            {
                BigInteger val = tokenId.and(attrtype.bitmask).shiftRight(attrtype.bitshift);
                result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, attrtype.processValue(val), attrtype.getSyntaxVal(attrtype.toString(val)));
            }
        }
        catch (Exception e)
        {
            result = new TokenScriptResult.Attribute(attrtype.name, attrtype.label, tokenId, "unsupported encoding");
        }

        return result;
    }

    public TokenScriptResult.Attribute getAttribute(Token token, BigInteger tokenId, String attribute)
    {
        TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        if (definition != null && definition.attributes.containsKey(attribute))
        {
            return getTokenscriptAttr(definition, tokenId, attribute);
        }
        else
        {
            return null;
        }
    }

    private boolean checkReadPermission() {
        return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
                == PackageManager.PERMISSION_GRANTED;
    }

    private TokenDefinition getDefinition(int chainId, String address)
    {
        TokenDefinition result = null;
        //try cache
        if (cachedDefinition != null)
        {
            //only match holding token
            ContractInfo holdingContracts = cachedDefinition.contracts.get(cachedDefinition.holdingToken);
            if (holdingContracts != null && holdingContracts.addresses.containsKey(chainId))
            {
                for (String addr : holdingContracts.addresses.get(chainId))
                {
                    if (addr.equalsIgnoreCase(address.toLowerCase())) return cachedDefinition;
                }
            }
        }

        TokenScriptFile tf = getTokenScriptFile(chainId, address);
        try
        {
            if (tf.isValidTokenScript())
            {
                cachedDefinition = parseFile(tf.getInputStream());
                result = cachedDefinition;
            }
        }
        catch (NumberFormatException e)
        {
            //no action
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return result;
    }

    public TokenScriptFile getTokenScriptFile(int chainId, String address)
    {
        if (address.equalsIgnoreCase(tokensService.getCurrentAddress()))
        {
            address = "ethereum";
        }

        if (assetDefinitions.get(chainId) != null && assetDefinitions.get(chainId).containsKey(address))
        {
            return assetDefinitions.get(chainId).get(address);
        }
        else
        {
            return new TokenScriptFile();
        }
    }


    /**
     * Get asset definition given contract address
     *
     * @param address
     * @return
     */
    public TokenDefinition getAssetDefinition(int chainId, String address)
    {
        TokenDefinition assetDef = null;
        if (address == null) return null;

        if (address.equalsIgnoreCase(tokensService.getCurrentAddress()))
        {
            address = "ethereum";
        }
        //is asset definition currently read?
        assetDef = getDefinition(chainId, address.toLowerCase());
        if (assetDef == null && !address.equals("ethereum"))
        {
            //try web
            loadScriptFromServer(address.toLowerCase()); //this will complete asynchronously and display will be updated
        }

        return assetDef; // if nothing found use default
    }

    public Single<TokenDefinition> getAssetDefinitionASync(int chainId, final String address)
    {
        if (address == null) return Single.fromCallable(TokenDefinition::new);
        String contractName = address;
        if (contractName.equalsIgnoreCase(tokensService.getCurrentAddress())) contractName = "ethereum";

        // hold until asset definitions have finished loading
        waitForAssets();

        final TokenDefinition assetDef = getDefinition(chainId, contractName.toLowerCase());
        if (assetDef != null) return Single.fromCallable(() -> assetDef);
        else if (!contractName.equals("ethereum"))
        {
            return fetchXMLFromServer(contractName.toLowerCase())
                    .map(this::handleDefinitionFile);
        }
        else return Single.fromCallable(TokenDefinition::new);
    }

    public Single<List<ContractLocator>> getAllLoadedScripts()
    {
        return Single.fromCallable(() -> {
            waitForAssets();
            return getAllOriginContracts();
        });
    }

    private void waitForAssets()
    {
        try
        {
            assetLoadingLock.acquire();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        finally
        {
            assetLoadingLock.release();
        }
    }

    private TokenDefinition handleDefinitionFile(File tokenScriptFile)
    {
        if (tokenScriptFile != null && !tokenScriptFile.getName().equals("cache") && tokenScriptFile.canRead())
        {
            try (FileInputStream input = new FileInputStream(tokenScriptFile))
            {
                TokenDefinition token = parseFile(input);
                ContractInfo holdingContracts = token.contracts.get(token.holdingToken);
                if (holdingContracts != null)
                {
                    for (int network : holdingContracts.addresses.keySet())
                    {
                        addContractsToNetwork(network,
                                              networkAddresses(holdingContracts.addresses.get(network), tokenScriptFile.getAbsolutePath()));
                    }
                    return token;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return new TokenDefinition();
    }

    public String getTokenName(int chainId, String address, int count)
    {
        if (count > 2) count = 2;
        if (tokenTypeName.get(chainId) != null && tokenTypeName.get(chainId).containsKey(address)
                && tokenTypeName.get(chainId).get(address).get(count) != null)
        {
            return tokenTypeName.get(chainId).get(address).get(count);
        }
        else
        {
            TokenDefinition td = getAssetDefinition(chainId, address);
            if (td == null) return null;

            if (tokenTypeName.get(chainId) == null) tokenTypeName.put(chainId, new ConcurrentHashMap<>());
            if (!tokenTypeName.get(chainId).containsKey(address)) tokenTypeName.get(chainId).put(address, new SparseArray<>());
            tokenTypeName.get(chainId).get(address).put(count, td.getTokenName(count));
            return td.getTokenName(count);
        }
    }

    public String getTokenNameFromService(int chainId, String address)
    {
        Token token = tokensService.getToken(chainId, address);
        if (token != null) return token.getFullName();
        else return "";
    }

    public Token getTokenFromService(int chainId, String address)
    {
        return tokensService.getToken(chainId, address);
    }

    /**
     * Function returns all contracts on this network ID
     *
     * @param networkId
     * @return
     */
    public List<String> getAllContracts(int networkId)
    {
        Map<String, TokenScriptFile> networkList = assetDefinitions.get(networkId);
        if (networkList != null)
        {
            return new ArrayList<>(networkList.keySet());
        }
        else
        {
            return new ArrayList<>();
        }
    }

    /**
     * Get the issuer label given the contract address
     * Note: this is optimised so as we don't need to keep loading in definitions as the user scrolls
     *
     * @param token
     * @return
     */
    public String getIssuerName(Token token)
    {
        int chainId = token.tokenInfo.chainId;
        String address = token.tokenInfo.address;

        String issuer = token.getNetworkName();

        TokenDefinition td = getAssetDefinition(chainId, address);
        if (td == null) return issuer;

        try
        {
            TokenScriptFile tsf = getTokenScriptFile(chainId, address);
            XMLDsigDescriptor sig = getCertificateFromRealm(tsf.calcMD5());
            if (sig != null && sig.keyName != null) issuer = sig.keyName;
        }
        catch (Exception e)
        {
            System.out.println(token.getFullName());
            e.printStackTrace();
            // no action
        }

        return issuer;
    }

    private void loadScriptFromServer(String correctedAddress)
    {
        //first check the last time we tried this session
        if (assetChecked.get(correctedAddress) == null || (System.currentTimeMillis() > (assetChecked.get(correctedAddress) + 1000L*60L*60L)))
        {
            fetchXMLFromServer(correctedAddress)
                    .flatMap(this::cacheSignature)
                    .map(this::handleFileLoad)
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(this::loadComplete, this::onError).isDisposed();
        }
    }

    private void loadComplete(String fileName)
    {
        if (BuildConfig.DEBUG) System.out.println("TS LOAD: " + fileName);
    }

    private void onError(Throwable throwable)
    {
        throwable.printStackTrace();
    }

    private TokenDefinition parseFile(InputStream xmlInputStream) throws IOException, SAXException, Exception
    {
        Locale locale;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            locale = context.getResources().getConfiguration().getLocales().get(0);
        }
        else
        {
            locale = context.getResources().getConfiguration().locale;
        }

        return new TokenDefinition(
                xmlInputStream, locale, this);
    }

    private String handleFileLoad(File newFile) throws Exception
    {
        String fileLoad = "";
        if (newFile != null && !newFile.getName().equals("cache") && newFile.canRead())
        {
            List<ContractLocator> originContracts = addContractAddresses(newFile);
            Intent intent = new Intent(ADDED_TOKEN);
            intent.putParcelableArrayListExtra(C.EXTRA_TOKENID_LIST, (ArrayList)originContracts);
            context.sendBroadcast(intent);
            fileLoad = newFile.getName();
        }

        return fileLoad;
    }

    private Single<File> fetchXMLFromServer(String address)
    {
        return Single.fromCallable(() -> {
            final File defaultReturn = new File("");
            if (address.equals("")) return defaultReturn;

            File result = getDownloadedXMLFile(address);

            //peek to see if this file exists
            long fileTime = 0;
            if (result != null && result.exists())
            {
                TokenDefinition td = getTokenDefinition(result);
                if (definitionIsOutOfDate(td))
                {
                    removeFile(result.getAbsolutePath());
                    assetChecked.put(address, 0L);
                }
                else
                {
                    fileTime = result.lastModified();
                }
            }
            else
            {
                result = defaultReturn;
            }

            if (assetChecked.get(address) != null && (System.currentTimeMillis() > (assetChecked.get(address) + 1000L*60L*60L))) return result;

            SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH);
            format.setTimeZone(TimeZone.getTimeZone("UTC"));
            String dateFormat = format.format(new Date(fileTime));

            StringBuilder sb = new StringBuilder();
            sb.append(TOKENSCRIPT_REPO_SERVER);
            sb.append(TOKENSCRIPT_CURRENT_SCHEMA);
            sb.append("/");
            sb.append(address);

            //prepare Android headers
            PackageManager manager = context.getPackageManager();
            PackageInfo info = manager.getPackageInfo(
                    context.getPackageName(), 0);
            String appVersion = info.versionName;
            String OSVersion = String.valueOf(Build.VERSION.RELEASE);

            okhttp3.Response response = null;

            try
            {
                Request request = new Request.Builder()
                        .url(sb.toString())
                        .get()
                        .addHeader("Accept", "text/xml; charset=UTF-8")
                        .addHeader("X-Client-Name", "AlphaWallet")
                        .addHeader("X-Client-Version", appVersion)
                        .addHeader("X-Platform-Name", "Android")
                        .addHeader("X-Platform-Version", OSVersion)
                        .addHeader("If-Modified-Since", dateFormat)
                        .build();

                response = okHttpClient.newCall(request).execute();

                switch (response.code())
                {
                    case HttpURLConnection.HTTP_NOT_MODIFIED:
                        result = defaultReturn;
                        break;
                    case HttpURLConnection.HTTP_OK:
                        String xmlBody = response.body().string();
                        result = storeFile(address, xmlBody);
                        break;
                    default:
                        result = defaultReturn;
                        break;
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
            finally
            {
                if (response != null) response.body().close();
            }

            assetChecked.put(address, System.currentTimeMillis());

            return result;
        });
    }

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

    private void finishLoading()
    {
        assetLoadingLock.release();
        if (eventCallback != null)
        {
            generateAndSendEvents();
        }
        else
        {
            requireEventSend = true;
        }
        startEventListeners();
    }

    private void addContractsToNetwork(Integer network, Map<String, File> newTokenDescriptionAddresses)
    {
        String externalDir = context.getExternalFilesDir("").getAbsolutePath();
        if (assetDefinitions.get(network) == null) assetDefinitions.put(network, new ConcurrentHashMap<>());
        for (String address : newTokenDescriptionAddresses.keySet())
        {
            String newTsFile = newTokenDescriptionAddresses.get(address).getAbsolutePath();

            if (assetDefinitions.get(network).containsKey(address))
            {
                String existingFilename = assetDefinitions.get(network).get(address).getAbsolutePath();
                boolean existingFileIsDebug = existingFilename.contains(HomeViewModel.ALPHAWALLET_DIR)
                        || existingFilename.contains(externalDir);
                boolean newFileIsDebug = newTsFile.contains(HomeViewModel.ALPHAWALLET_DIR)
                        || newTsFile.contains(externalDir);

                //remove old file if it's an active update and file is in dev area
                if (!newTsFile.equals(existingFilename) && newFileIsDebug && existingFileIsDebug)
                {
                    //delete old developer override - could be a different filename which will cause trouble later
                    removeFile(existingFilename);
                }

                if (existingFileIsDebug && !newFileIsDebug) continue;
            }

            TokenScriptFile oldTsFile = assetDefinitions.get(network).put(address, new TokenScriptFile(context, newTsFile));
            if (oldTsFile != null && !oldTsFile.getAbsolutePath().equals(newTsFile))
            {
                System.out.println("TSOverride: " + newTsFile + " Overrides " + oldTsFile.getAbsolutePath());
            }
        }
    }

    private List<String> getAllNewFiles(Map<String, File> newTokenAddresses)
    {
        List<String> newFiles = new ArrayList<>();
        for (File f : newTokenAddresses.values())
        {
            newFiles.add(f.getAbsolutePath());
        }

        return newFiles;
    }

    private void removeFile(String filename)
    {
        try
        {
            File fileToDelete = new File(filename);
            fileToDelete.delete();
        }
        catch (Exception e)
        {
            //ignore error
        }
    }

    private Map<String, File> networkAddresses(List<String> strings, String path)
    {
        Map<String, File> addrMap = new ConcurrentHashMap<>();
        for (String address : strings) addrMap.put(address, new File(path));
        return addrMap;
    }


    private boolean addContractAssets(String asset)
    {
        try (InputStream input = context.getResources().getAssets().open(asset)) {
            TokenDefinition token = parseFile(input);
            TokenScriptFile tsf = new TokenScriptFile(context, asset);
            ContractInfo holdingContracts = token.contracts.get(token.holdingToken);
            if (holdingContracts != null)
            {
                //some Android versions don't have stream()
                for (int network : holdingContracts.addresses.keySet())
                {
                    addContractsToNetwork(network, networkAddresses(holdingContracts.addresses.get(network), asset));
                    XMLDsigDescriptor AWSig = new XMLDsigDescriptor();
                    String hash = tsf.calcMD5();
                    AWSig.result = "pass";
                    AWSig.issuer = "AlphaWallet";
                    AWSig.keyName = "AlphaWallet";
                    AWSig.type = SigReturnType.SIGNATURE_PASS;
                    tsf.determineSignatureType(AWSig);
                    storeCertificateData(hash, AWSig);
                }
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    public TokenDefinition getTokenDefinition(File file)
    {
        try (FileInputStream input = new FileInputStream(file)) {
            return parseFile(input);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return null;
    }

    private List<ContractLocator> addContractAddresses(File file) throws Exception
    {
        FileInputStream input = new FileInputStream(file);
        return addContractAddresses(parseFile(input), file);
    }

    private List<ContractLocator> addContractAddresses(TokenDefinition tokenDef, File file) throws Exception
    {
        ContractInfo holdingContracts = tokenDef.contracts.get(tokenDef.holdingToken);

        if (holdingContracts != null)
        {
            addToEventList(tokenDef);
            for (int network : holdingContracts.addresses.keySet())
            {
                addContractsToNetwork(network, networkAddresses(holdingContracts.addresses.get(network), file.getAbsolutePath()));
            }

            return ContractLocator.fromContractInfo(holdingContracts);
        }

        return new ArrayList<>();
    }

    private void addToEventList(TokenDefinition tokenDef)
    {
        for (String attrName : tokenDef.attributes.keySet())
        {
            Attribute attr = tokenDef.attributes.get(attrName);
            if (attr.event != null && attr.event.contract != null)
            {
                eventList.add(attr.event); //note: event definition contains link back to the contract it refers to
            }
        }
    }

    private void stopEventListener()
    {
        if (eventListener != null) eventListener.dispose();
        //blank all events
        for (EventDefinition ev : eventList)
        {
            ev.readBlock = BigInteger.ZERO;
        }
    }

    public void startEventListeners()
    {
        stopEventListener();
        eventListener =  Observable.interval(0, CHECK_TX_LOGS_INTERVAL, TimeUnit.SECONDS)
                .doOnNext(l -> {
                    checkEvents()
                            .subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread())
                            .subscribe(() -> {}, t -> {}) //results are handled within logging function
                            .isDisposed();
                }).subscribe();
    }

    private Completable checkEvents()
    {
        //check events for corresponding tokens
        return Completable.fromAction(() -> {
            for (EventDefinition ev : eventList)
            {
                try
                {
                    getEvents(ev);
                }
                catch (Exception e)
                {
                    //TODO: Handle event issues
                    e.printStackTrace();
                }
            }
        });
    }

    private void getEvents(EventDefinition ev) throws Exception
    {
        int chainId = ev.contract.addresses.keySet().iterator().next();
        String address = ev.contract.addresses.get(chainId).get(0);

        int originChainId = ev.parentAttribute.originContract.addresses.keySet().iterator().next();
        String originAddress = ev.parentAttribute.originContract.addresses.get(originChainId).get(0);

        Token originToken = tokensService.getToken(originChainId, originAddress);
        if (originToken == null ||
                (originToken.isNonFungible() && !originToken.hasPositiveBalance())) return; // early return if NFT and wallet has zero balance for this token
                                                                                            // Note: Fungible with zero balance is safe to query events as filter is always ownerAddress
        Web3j web3j = getWeb3jService(chainId);

        final EthFilter filter = eventUtils.generateLogFilter(ev, originToken, this);

        try
        {
            eventConnection.acquire(); //prevent overlapping event calls
            EthLog ethLogs = web3j.ethGetLogs(filter).send();
            processLogs(ev, ethLogs.getLogs());
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            eventConnection.release();
        }

        //More elegant, but requires a private node
//        return web3j.ethLogFlowable(filter)
//                .subscribeOn(Schedulers.io())
//                .observeOn(AndroidSchedulers.mainThread())
//                .subscribe(log -> {
//            System.out.println("log.toString(): " +  log.toString());
//            //TODO here: callback to event service listener
//        }, this::onLogError);
    }

    private void processLogs(EventDefinition ev, List<EthLog.LogResult> logs)
    {
        if (logs.size() == 0) return; //early return
        int chainId = ev.contract.addresses.keySet().iterator().next();
        Web3j web3j = getWeb3jService(chainId);

        for (EthLog.LogResult ethLog : logs)
        {
            String selectVal = eventUtils.getSelectVal(ev, ethLog);
            EthBlock txBlock = eventUtils.getTransactionDetails(((Log)ethLog.get()).getBlockHash(), web3j).blockingGet();

            long blockTime = txBlock.getBlock().getTimestamp().longValue();
            if (eventCallback != null) eventCallback.receivedEvent(ev.attributeName, ev.parentAttribute.getSyntaxVal(selectVal), blockTime, chainId);
            storeEventValue(ev, ethLog, ev.parentAttribute, blockTime, selectVal);
        }
    }

    private void storeEventValue(EventDefinition ev, EthLog.LogResult log, Attribute attr,
                                 long blockTime, String selectVal)
    {
        //store result
        String filterTopicValue = ev.getFilterTopicValue();
        TransactionResult txResult;
        BigInteger tokenId;

        if (filterTopicValue.equals("tokenId"))
        {
            String tokenIdStr = eventUtils.getTopicVal(ev, log);
            if (tokenIdStr.startsWith("0x"))
            {
                tokenId = Numeric.toBigInt(tokenIdStr);
            }
            else
            {
                tokenId = new BigInteger(tokenIdStr);
            }
        }
        else
        {
            tokenId = BigInteger.ZERO;
        }

        ContractAddress eventContractAddress = new ContractAddress(attr.event.contract.addresses.keySet().iterator().next(),
                                                                   attr.event.contract.addresses.values().iterator().next().get(0));
        txResult = getFunctionResult(eventContractAddress, attr, tokenId);

        txResult.result = selectVal;

        if (txResult.resultTime == 0 || blockTime >= txResult.resultTime)
        {
            //store
            txResult.resultTime = blockTime;
            storeAuxData(txResult); // updates the entry for the attribute
            ev.hasNewEvent = true;
        }

        txResult.resultTime = blockTime;
        txResult.result = attr.getSyntaxVal(selectVal) + "," + ev.readBlock.toString(16); //store block time as well as block number
        storeAuxData(txResult); //store the event itself
    }

    public boolean checkTokenForNewEvent(Token token)
    {
        boolean hasEvent = false;
        for (EventDefinition ev : eventList)
        {
            if (ev.eventModule == null || ev.contract == null) continue;
            if (ev.contract.addresses.containsKey(token.tokenInfo.chainId))
            {
                if (ev.contract.addresses.get(token.tokenInfo.chainId).contains(token.getAddress().toLowerCase()))
                {
                    hasEvent = ev.hasNewEvent;
                    ev.hasNewEvent = false;
                    break;
                }
            }
        }

        return hasEvent;
    }

    private void onLogError(Throwable throwable)
    {
        System.out.println("Log error: " + throwable.getMessage());
        throwable.printStackTrace();
    }

    private boolean allowableExtension(File file)
    {
        int index = file.getName().lastIndexOf(".");
        if (index >= 0)
        {
            String extension = file.getName().substring(index+1);
            switch (extension)
            {
                case "xml":
                case "tsml":
                    return true;
                default:
                    break;
            }
        }

        return false;
    }

    private String getFileName(File file)
    {
        String name = file.getName();
        int index = name.lastIndexOf(".");
        if (index > 0)
        {
            return name.substring(0, index);
        }
        else
        {
            return null;
        }
    }

    private boolean isAddress(File file)
    {
        String name = getFileName(file);
        if (name != null) return Utils.isAddressValid(name);
        else return false;
    }

    /**
     * This is used to retrieve the file from the secure area in order to check the date.
     * Note: it only finds files previously downloaded from the server
     * @param contractAddress
     * @return
     */
    private File getDownloadedXMLFile(String contractAddress)
    {
        //if in secure area will simply be address + XML
        String filename = contractAddress + ".xml";
        File file = new File(context.getFilesDir(), filename);
        if (file.exists() && file.canRead())
        {
            return file;
        }

        File[] files = context.getFilesDir().listFiles();
        for (File f : files)
        {
            if (f.getName().equalsIgnoreCase(filename)) return f;
        }

        return null;
    }

    private List<String> getScriptsInSecureZone()
    {
        List<String> checkScripts = new ArrayList<>();
        File[] files = context.getFilesDir().listFiles();
        Observable.fromArray(files)
                .filter(File::isFile)
                .filter(this::allowableExtension)
                .filter(File::canRead)
                .filter(this::isAddress)
                .forEach(file -> checkScripts.add(getFileName(file)) ).isDisposed();

        return checkScripts;
    }

    private boolean isInSecureZone(File file)
    {
        return file.getPath().contains(context.getFilesDir().getPath());
    }

    /**
     * check the repo server for updates to downloaded TokenScripts
     */
    private void checkDownloadedFiles()
    {
        Observable.fromIterable(getScriptsInSecureZone())
                .concatMap(addr -> fetchXMLFromServer(addr).toObservable())
                .map(this::handleFileLoad)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::loadComplete, this::onError).isDisposed();
    }

    /* Add cached signature if uncached files found. */
    private Single<File> cacheSignature(File file)
    {
        // note that outdated cache is never deleted - we don't have that level of finesse:
        // Note from developer to commenter above: outdated certificate is simply replaced in the realm - there's no history.
        //      However there is an issue here - if the tokenscript is removed then this entry will be orphaned.
        //      Once we cache the tokenscript contracts we will know if the script has been removed and can remove this file too.
        return Single.fromCallable(() -> {
            if (file.canRead())
            {
                TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath());
                String hash = tsf.calcMD5();

                //pull data from realm
                XMLDsigDescriptor sig = getCertificateFromRealm(hash);
                if (sig == null || sig.keyName == null)
                {
                    //fetch signature and store in realm
                    sig = alphaWalletService.checkTokenScriptSignature(tsf);
                    tsf.determineSignatureType(sig);
                    storeCertificateData(hash, sig);
                }
            }

            return file;
        });
    }

    private void storeCertificateData(String hash, XMLDsigDescriptor sig)
    {
        try (Realm realm = realmManager.getRealmInstance(CERTIFICATE_DB))
        {
            //if signature present, then just update
            RealmCertificateData realmData = realm.where(RealmCertificateData.class)
                    .equalTo("instanceKey", hash)
                    .findFirst();

            TransactionsRealmCache.addRealm();
            realm.beginTransaction();
            if (realmData == null) realmData = realm.createObject(RealmCertificateData.class, hash);
            realmData.setFromSig(sig);
            realm.commitTransaction();
            realm.close();
            TransactionsRealmCache.subRealm();
        }
        catch (Exception e)
        {
            TransactionsRealmCache.subRealm();
            e.printStackTrace();
        }
    }

    private XMLDsigDescriptor getCertificateFromRealm(String hash)
    {
        XMLDsigDescriptor sig = null;
        try (Realm realm = realmManager.getRealmInstance(CERTIFICATE_DB))
        {
            RealmCertificateData realmCert = realm.where(RealmCertificateData.class)
                    .equalTo("instanceKey", hash)
                    .findFirst();

            if (realmCert != null)
            {
                sig = new XMLDsigDescriptor();
                sig.issuer = realmCert.getIssuer();
                sig.certificateName = realmCert.getCertificateName();
                sig.keyName = realmCert.getKeyName();
                sig.keyType = realmCert.getKeyType();
                sig.result = realmCert.getResult();
                sig.subject = realmCert.getSubject();
                sig.type = realmCert.getType();
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return sig;
    }

    /**
     * Use internal directory to store contracts fetched from the server
     * @param address
     * @param result
     * @return
     * @throws
     */
    private File storeFile(String address, String result) throws IOException
    {
        if (result == null || result.length() < 10) return null;

        String fName = address + ".xml";

        //Store received files in the internal storage area - no need to ask for permissions
        File file = new File(context.getFilesDir(), fName);

        FileOutputStream fos = new FileOutputStream(file);
        OutputStream os = new BufferedOutputStream(fos);
        os.write(result.getBytes());
        fos.flush();
        os.close();
        fos.close();
        return file;
    }

    public boolean hasDefinition(int chainId, String address)
    {
        if (address.equalsIgnoreCase(tokensService.getCurrentAddress()))
        {
            address = "ethereum";
        }
        return assetDefinitions.get(chainId) != null && assetDefinitions.get(chainId).containsKey(address);
    }

    //when user reloads the tokens we should also check XML for any files
    public void clearCheckTimes()
    {
        assetChecked.clear();
    }

    public boolean hasTokenView(int chainId, String contractAddr, String type)
    {
        TokenDefinition td = getAssetDefinition(chainId, contractAddr);
        return td != null && td.hasTokenView();
    }

    public String getTokenView(int chainId, String contractAddr, String type)
    {
        String viewHTML = "";
        TokenDefinition td = getAssetDefinition(chainId, contractAddr);
        if (td != null)
        {
            viewHTML = td.getTokenView(type);
        }

        return viewHTML;
    }

    public String getTokenViewStyle(int chainId, String contractAddr, String type)
    {
        String styleData = "";
        TokenDefinition td = getAssetDefinition(chainId, contractAddr);
        if (td != null)
        {
            styleData = td.getTokenViewStyle(type);
        }

        return styleData;
    }

    public List<Attribute> getTokenViewLocalAttributes(int chainId, String contractAddr)
    {
        TokenDefinition td = getAssetDefinition(chainId, contractAddr);
        List<Attribute> results = new ArrayList<>();
        if (td != null)
        {
            Map<String, Attribute> attrMap = td.getTokenViewLocalAttributes();
            results.addAll(attrMap.values());
        }

        return results;
    }

    public Map<String, TSAction> getTokenFunctionMap(int chainId, String contractAddr)
    {
        TokenDefinition td = getAssetDefinition(chainId, contractAddr);
        if (td != null)
        {
            return td.getActions();
        }
        else
        {
            return null;
        }
    }

    /**
     * Build a map of all available tokenIds to a list of available functions for that tokenId
     *
     * @param token
     * @return map of unique tokenIds to lists of allowed functions for that ID - note that we allow the function to be displayed if it has a denial message
     */
    public Single<Map<BigInteger, List<String>>> fetchFunctionMap(Token token)
    {
        return Single.fromCallable(() -> {
            Map<BigInteger, List<String>> validActions = new HashMap<>();
            TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress());
            if (td != null)
            {
                List<BigInteger> tokenIds = token.getUniqueTokenIds();
                Map<String, TSAction> actions = td.getActions();
                //first gather all attrs required - do this so if there's multiple actions using the same attribute for a tokenId we aren't fetching the value repeatedly
                List<String> requiredAttrNames = getRequiredAttributeNames(actions, td);
                Map<BigInteger, Map<String, TokenScriptResult.Attribute>> attrResults   // Map of attribute results vs tokenId
                        = getRequiredAttributeResults(requiredAttrNames, tokenIds, td, token); // Map of all required attribute values vs all the tokenIds

                for (BigInteger tokenId : tokenIds)
                {
                    for (String actionName : actions.keySet())
                    {
                        TSAction action = actions.get(actionName);
                        TSSelection selection = action.exclude != null ? td.getSelection(action.exclude) : null;
                        if (selection == null)
                        {
                            if (!validActions.containsKey(tokenId)) validActions.put(tokenId, new ArrayList<>());
                            validActions.get(tokenId).add(actionName);
                        }
                        else
                        {
                            //get required Attribute Results for this tokenId & selection
                            List<String> requiredAttributeNames = selection.getRequiredAttrs();
                            Map<String, TokenScriptResult.Attribute> idAttrResults = getAttributeResultsForTokenIds(attrResults, requiredAttributeNames, tokenId);
                            addIntrinsicAttributes(idAttrResults, token, tokenId); //adding intrinsic attributes eg ownerAddress, tokenId, contractAddress

                            //Now evaluate the selection
                            boolean exclude = EvaluateSelection.evaluate(selection.head, idAttrResults);
                            if (!exclude || selection.denialMessage != null)
                            {
                                if (!validActions.containsKey(tokenId)) validActions.put(tokenId, new ArrayList<>());
                                validActions.get(tokenId).add(actionName);
                            }
                        }
                    }
                }
            }

            return validActions;
        });
    }

    private void addIntrinsicAttributes(Map<String, TokenScriptResult.Attribute> attrs, Token token, BigInteger tokenId)
    {
        //add tokenId, ownerAddress & contractAddress
        attrs.put("tokenId", new TokenScriptResult.Attribute("tokenId", "tokenId", tokenId, tokenId.toString(10)));
        attrs.put("ownerAddress", new TokenScriptResult.Attribute("ownerAddress", "ownerAddress", BigInteger.ZERO, token.getWallet()));
        attrs.put("contractAddress", new TokenScriptResult.Attribute("contractAddress", "contractAddress", BigInteger.ZERO, token.getAddress()));
    }

    public String checkFunctionDenied(Token token, String actionName, List<BigInteger> tokenIds)
    {
        String denialMessage = null;
        TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress());
        if (td != null)
        {
            BigInteger tokenId = tokenIds != null ? tokenIds.get(0) : BigInteger.ZERO;
            TSAction action = td.actions.get(actionName);
            TSSelection selection = action.exclude != null ? td.getSelection(action.exclude) : null;
            if (selection != null)
            {
                //gather list of attribute results
                List<String> requiredAttrs = selection.getRequiredAttrs();
                //resolve all these attrs
                Map<String, TokenScriptResult.Attribute> attrs = new HashMap<>();
                //get results
                for (String attrId : requiredAttrs)
                {
                    Attribute attr = td.attributes.get(attrId);
                    if (attr == null) continue;
                    TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, tokenId, td, this, false).blockingSingle();
                    if (attrResult != null) attrs.put(attrId, attrResult);
                }

                addIntrinsicAttributes(attrs, token, tokenId);

                boolean exclude = EvaluateSelection.evaluate(selection.head, attrs);
                if (exclude && !TextUtils.isEmpty(selection.denialMessage))
                {
                    denialMessage = selection.denialMessage;
                }
            }
        }

        return denialMessage;
    }

    private Map<String, TokenScriptResult.Attribute> getAttributeResultsForTokenIds(Map<BigInteger, Map<String, TokenScriptResult.Attribute>> attrResults, List<String> requiredAttributeNames, BigInteger tokenId)
    {
        Map<String, TokenScriptResult.Attribute> results = new HashMap<>();
        if (!attrResults.containsKey(tokenId)) return results; //check values

        for (String attributeName : requiredAttributeNames)
        {
            results.put(attributeName, attrResults.get(tokenId).get(attributeName));
        }

        return results;
    }

    private Map<BigInteger, Map<String, TokenScriptResult.Attribute>> getRequiredAttributeResults(List<String> requiredAttrNames, List<BigInteger> tokenIds, TokenDefinition td, Token token)
    {
        Map<BigInteger, Map<String, TokenScriptResult.Attribute>> resultSet = new HashMap<>();
        for (BigInteger tokenId : tokenIds)
        {
            for (String attrName : requiredAttrNames)
            {
                Attribute attr = td.attributes.get(attrName);
                if (attr == null) continue;
                TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, tokenId, td, this, false).blockingSingle();
                if (attrResult != null)
                {
                    Map<String, TokenScriptResult.Attribute> tokenIdMap = resultSet.get(tokenId);
                    if (tokenIdMap == null)
                    {
                        tokenIdMap = new HashMap<>();
                        resultSet.put(tokenId, tokenIdMap);
                    }
                    tokenIdMap.put(attrName, attrResult);
                }
            }
        }

        return resultSet;
    }

    private List<String> getRequiredAttributeNames(Map<String, TSAction> actions, TokenDefinition td)
    {
        List<String> requiredAttrs = new ArrayList<>();
        for (String actionName : actions.keySet())
        {
            TSAction action = actions.get(actionName);
            TSSelection selection = action.exclude != null ? td.getSelection(action.exclude) : null;
            if (selection != null)
            {
                List<String> attrNames = selection.getRequiredAttrs();
                for (String attrName : attrNames)
                {
                    if (!requiredAttrs.contains(attrName))
                        requiredAttrs.add(attrName);
                }
            }
        }

        return requiredAttrs;
    }

    @Override
    public void parseMessage(ParseResultId parseResult)
    {
        switch (parseResult)
        {
            case PARSER_OUT_OF_DATE:
                HomeActivity.setUpdatePrompt();
                break;
            case XML_OUT_OF_DATE:
                break;
            case OK:
                break;
        }
    }

    private FileObserver startFileListener(String path)
    {
        FileObserver observer = new FileObserver(path)
        {
            private final String listenerPath = path;
            @Override
            public void onEvent(int event, @Nullable String file)
            {
                //watch for new files and file change
                switch (event)
                {
                    case CREATE:
                        //if this file already exists then wait for the modify
                        File checkFile = new File(listenerPath, file);
                        if (checkFile.exists() && checkFile.canRead())
                        {
                            break;
                        }
                    case MODIFY:
                        try
                        {
                            if (file.contains(".xml") || file.contains(".tsml"))
                            {
                                System.out.println("FILE: " + file);
                                //form filename
                                TokenScriptFile newTSFile = new TokenScriptFile(context, listenerPath, file);
                                List<ContractLocator> originContracts = addContractAddresses(newTSFile);

                                if (originContracts.size() > 0)
                                {
                                    notificationService.DisplayNotification("Definition Updated", file, NotificationCompat.PRIORITY_MAX);
                                    cachedDefinition = null;
                                    cacheSignature(newTSFile) //update signature data if necessary
                                            .subscribeOn(Schedulers.io())
                                            .observeOn(AndroidSchedulers.mainThread())
                                            .subscribe().isDisposed();

                                    Intent intent = new Intent(ADDED_TOKEN);
                                    intent.putParcelableArrayListExtra(C.EXTRA_TOKENID_LIST, (ArrayList)originContracts);
                                    context.sendBroadcast(intent);
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            if (homeMessenger != null) homeMessenger.tokenScriptError(e.getMessage());
                        }
                        break;
                    default:
                        break;
                }
            }
        };

        observer.startWatching();

        return observer;
    }

    public void checkTokenscriptEnabledTokens(TokensService tokensService)
    {
        for (int i = 0; i < assetDefinitions.size(); i++)
        {
            int networkId = assetDefinitions.keyAt(i);
            Map<String, TokenScriptFile> defMap = assetDefinitions.valueAt(i);
            for (String address : defMap.keySet())
            {
                Token token = tokensService.getToken(networkId, address);
                if (token != null)
                {
                    TokenScriptFile tokenDef = defMap.get(address);
                    token.hasTokenScript = true;
                    TokenDefinition td = getAssetDefinition(networkId, address);
                    if (td != null && td.contracts != null)
                    {
                        ContractInfo cInfo = td.contracts.get(td.holdingToken);
                        if (cInfo != null) checkCorrectInterface(token, cInfo.contractInterface);
                    }
                }
            }
        }
    }

    public Single<XMLDsigDescriptor> getSignatureData(int chainId, String contractAddress)
    {
        return Single.fromCallable(() -> {
            XMLDsigDescriptor sigDescriptor = new XMLDsigDescriptor();
            sigDescriptor.result = "fail";
            sigDescriptor.type = SigReturnType.NO_TOKENSCRIPT;

            TokenScriptFile tsf = getTokenScriptFile(chainId, contractAddress);
            if (tsf != null && tsf.isValidTokenScript())
            {
                String hash = tsf.calcMD5();
                XMLDsigDescriptor sig = getCertificateFromRealm(hash);
                if (sig == null)
                {
                    sig = alphaWalletService.checkTokenScriptSignature(tsf);
                    tsf.determineSignatureType(sig);
                    storeCertificateData(hash, sig);
                }
                sigDescriptor = sig;
            }

            return sigDescriptor;
        });
    }

    private void checkCorrectInterface(Token token, String contractInterface)
    {
        ContractType cType;
        switch (contractInterface.toLowerCase())
        {
            case "erc875":
                cType = ContractType.ERC875;
                if (token.isERC875()) return;
                break;
            case "erc20":
                cType = ContractType.ERC20;
                break;
            // note: ERC721 and ERC721Ticket are contracts with different interfaces which are handled in different ways but we describe them
            // as the same within the tokenscript.
            case "erc721":
                if (token.isERC721() || token.isERC721Ticket()) return;
                cType = ContractType.ERC721;
                break;
            case "erc721ticket":
                if (token.isERC721() || token.isERC721Ticket()) return;
                cType = ContractType.ERC721_TICKET;
                break;
            case "ethereum":
                cType = ContractType.ETHEREUM;
                break;
            default:
                cType = ContractType.OTHER;
                break;
        }

        if (cType == ContractType.OTHER) return;
        if (cType == token.getInterfaceSpec()) return;

        //contract mismatch, re-assign
        //first delete from database
        tokenLocalSource.deleteRealmToken(token.tokenInfo.chainId, new Wallet(token.getWallet()), token.tokenInfo.address);

        //now store into database
        //TODO: if erc20 refresh all values
        TokenFactory tf = new TokenFactory();

        Token newToken = tf.createToken(token.tokenInfo, BigDecimal.ZERO, null, 0, cType, token.getNetworkName(), 0);
        newToken.setTokenWallet(token.getWallet());
        newToken.walletUIUpdateRequired = true;
        newToken.updateBlancaTime = 0;
        newToken.transferPreviousData(token);

        tokenLocalSource.saveToken(new Wallet(token.getWallet()), newToken)
                .subscribeOn(Schedulers.io())
                .subscribe(tokensService::addToken).isDisposed();
    }


    //Database functions
    private String functionKey(ContractAddress cAddr, BigInteger tokenId, String attrId)
    {
        //produce a unique key for this. token address, token Id, chainId
        return cAddr.address + "-" + tokenId.toString(Character.MAX_RADIX) + "-" + cAddr.chainId + "-" + attrId;
    }

    private String eventKey(TransactionResult tResult)
    {
        return tResult.contractAddress + "-" + tResult.tokenId.toString(Character.MAX_RADIX) + "-" + tResult.contractChainId + "-" + tResult.attrId + tResult.resultTime + "-log";
    }

    @Override
    public TransactionResult getFunctionResult(ContractAddress contract, Attribute attr, BigInteger tokenId)
    {
        TransactionResult tr = new TransactionResult(contract.chainId, contract.address, tokenId, attr);
        String dataBaseKey = functionKey(contract, tokenId, attr.name);
        try (Realm realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress()))
        {
            RealmAuxData realmToken = realm.where(RealmAuxData.class)
                    .equalTo("instanceKey", dataBaseKey)
                    .equalTo("chainId", contract.chainId)
                    .findFirst();

            if (realmToken != null)
            {
                tr.resultTime = realmToken.getResultTime();
                tr.result = realmToken.getResult();
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return tr;
    }

    @Override
    public TransactionResult storeAuxData(TransactionResult tResult)
    {
        Completable.complete()
                .subscribeWith(new DisposableCompletableObserver()
                {
                    Realm realm = null;

                    @Override
                    public void onStart()
                    {
                        if (tokensService.getCurrentAddress() == null || !WalletUtils.isValidAddress(tokensService.getCurrentAddress())) return;
                        if (tResult.result == null || tResult.resultTime < 0) return;
                        realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress());
                        ContractAddress cAddr = new ContractAddress(tResult.contractChainId, tResult.contractAddress);
                        String databaseKey = functionKey(cAddr, tResult.tokenId, tResult.attrId);
                        if (tResult.result.contains(","))
                        {
                            databaseKey = eventKey(tResult);
                        }
                        RealmAuxData realmToken = realm.where(RealmAuxData.class)
                                .equalTo("instanceKey", databaseKey)
                                .equalTo("chainId", tResult.contractChainId)
                                .findFirst();

                        if (realmToken == null)
                        {
                            TransactionsRealmCache.addRealm();
                            realm.beginTransaction();
                            createAuxData(realm, tResult, databaseKey);
                        }
                        else if (tResult.result != null)
                        {
                            TransactionsRealmCache.addRealm();
                            realm.beginTransaction();
                            realmToken.setResult(tResult.result);
                            realmToken.setResultTime(tResult.resultTime);
                        }
                    }

                    @Override
                    public void onComplete()
                    {
                        if (realm != null)
                        {
                            if (realm.isInTransaction())
                            {
                                realm.commitTransaction();
                                TransactionsRealmCache.subRealm();
                            }
                            if (!realm.isClosed()) realm.close();
                        }
                    }

                    @Override
                    public void onError(Throwable e)
                    {
                        if (realm != null && !realm.isClosed())
                        {
                            if (realm.isInTransaction()) TransactionsRealmCache.subRealm();
                            realm.close();
                        }
                    }
                }).isDisposed();

        return tResult;
    }

    private void generateAndSendEvents()
    {
        List<Event> storedEvents = new ArrayList<>();
        try (Realm realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress()))
        {
            RealmResults<RealmAuxData> realmEvents = realm.where(RealmAuxData.class)
                    .endsWith("instanceKey", "-log")
                    .sort("resultTime", Sort.ASCENDING)
                    .findAll();

            for (RealmAuxData eventData : realmEvents)
            {
                String[] results = eventData.getResult().split(",");
                if (results.length != 2) continue;
                String result = results[0];
                BigInteger blockNumber = new BigInteger(results[1], 16);
                String eventText = "Event: " + eventData.getFunctionId() + " becomes " + result;
                Event ev = new Event(eventText, eventData.getResultTime(), eventData.getChainId());
                storedEvents.add(ev);

                //load event with top block value
                updateEventList(eventData, blockNumber);
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

        if (eventCallback != null) eventCallback.eventsLoaded(storedEvents.toArray(new Event[0]));
    }

    private void updateEventList(RealmAuxData eventData, BigInteger blockNumber)
    {
        for (EventDefinition ev : eventList)
        {
            if (ev.attributeName.equals(eventData.getFunctionId()))
            {
                //does the event module correspond to this contract?
                String[] contractDetails = eventData.getInstanceKey().split("-");
                //return tResult.contractAddress + "-" + tResult.tokenId.toString(Character.MAX_RADIX) + "-" + tResult.contractChainId + "-" + tResult.attrId + tResult.resultTime + "-log";
                ev.readBlock = blockNumber;
            }
        }
    }

    private void createAuxData(Realm realm, TransactionResult tResult, String dataBaseKey)
    {
        try
        {
            //ContractAddress cAddr = new ContractAddress(tResult.contractChainId, tResult.contractAddress);
            RealmAuxData realmData = realm.createObject(RealmAuxData.class, dataBaseKey);
            realmData.setResultTime(tResult.resultTime);
            realmData.setResult(tResult.result);
            realmData.setChainId(tResult.contractChainId);
            realmData.setFunctionId(tResult.method);
            realmData.setTokenId(tResult.tokenId.toString(Character.MAX_RADIX));
        }
        catch (RealmPrimaryKeyConstraintException e)
        {
            //in theory we should never see this
            e.printStackTrace();
        }
    }


    //private Token

    private void addOpenSeaAttributes(StringBuilder attrs, Token erc721Token, BigInteger tokenId)
    {
        Asset tokenAsset = erc721Token.getAssetForToken(tokenId.toString());
        if(tokenAsset == null) return;

        try
        {
            if (tokenAsset.getBackgroundColor() != null) TokenScriptResult.addPair(attrs, "background_colour", URLEncoder.encode(tokenAsset.getBackgroundColor(), "utf-8"));
            if (tokenAsset.getImagePreviewUrl() != null) TokenScriptResult.addPair(attrs, "image_preview_url", URLEncoder.encode(tokenAsset.getImagePreviewUrl(), "utf-8"));
            if (tokenAsset.getDescription() != null) TokenScriptResult.addPair(attrs, "description", URLEncoder.encode(tokenAsset.getDescription(), "utf-8"));
            if (tokenAsset.getExternalLink() != null) TokenScriptResult.addPair(attrs, "external_link", URLEncoder.encode(tokenAsset.getExternalLink(), "utf-8"));
            if (tokenAsset.getTraits() != null) TokenScriptResult.addPair(attrs, "traits", tokenAsset.getTraits());
            if (tokenAsset.getName() != null) TokenScriptResult.addPair(attrs, "name", URLEncoder.encode(tokenAsset.getName(), "utf-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            //
        }
    }

    public StringBuilder getTokenAttrs(Token token, BigInteger tokenId, int count)
    {
        StringBuilder attrs = new StringBuilder();

        TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        String name = token.getTokenTitle();
        if (definition != null && definition.getTokenName(1) != null)
        {
            name = definition.getTokenName(1);
        }
        TokenScriptResult.addPair(attrs, "name", name);
        TokenScriptResult.addPair(attrs, "label", name);
        TokenScriptResult.addPair(attrs, "symbol", token.getSymbol());
        TokenScriptResult.addPair(attrs, "_count", String.valueOf(count));
        TokenScriptResult.addPair(attrs, "contractAddress", token.tokenInfo.address);
        TokenScriptResult.addPair(attrs, "chainId", String.valueOf(token.tokenInfo.chainId));
        TokenScriptResult.addPair(attrs, "tokenId", tokenId);
        TokenScriptResult.addPair(attrs, "ownerAddress", token.getWallet());

        if(token instanceof ERC721Token)
        {
            addOpenSeaAttributes(attrs, token, tokenId);
        }

        if (token.isEthereum())
        {
            TokenScriptResult.addPair(attrs, "balance", token.balance.toString());
        }

        return attrs;
    }

    /**
     * Get all the magic values - eg native crypto balances for all chains
     * @return
     */
    public String getMagicValuesForInjection(int chainId) throws Exception
    {
        String walletBalance = "walletBalance";
        String prefix = "web3.eth";
        StringBuilder sb = new StringBuilder();
        sb.append("\n\n");
        Token nativeCurrency = tokensService.getToken(chainId, tokensService.getCurrentAddress());
        sb.append(prefix).append(" = {\n").append(walletBalance).append(": ").append(nativeCurrency.balance.toString()).append("\n}\n");

        List<Token> nativeCurrencies = tokensService.getAllAtAddress(tokensService.getCurrentAddress());
        for (Token currency : nativeCurrencies)
        {
            sb.append(prefix).append("_").append(currency.tokenInfo.chainId).append(" = {\n").append(walletBalance).append(": ").append(currency.balance.toString()).append("\n}\n");
        }

        sb.append("\n\n");

        return sb.toString();
    }

    public void clearResultMap()
    {
        tokenscriptUtility.clearParseMaps();
    }

    public Observable<TokenScriptResult.Attribute> resolveAttrs(Token token, BigInteger tokenId, List<Attribute> extraAttrs, boolean itemView)
    {
        TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        ContractAddress cAddr = new ContractAddress(token.tokenInfo.chainId, token.tokenInfo.address);
        if (definition == null) return Observable.fromCallable(() -> new TokenScriptResult.Attribute("RAttrs", "", BigInteger.ZERO, ""));

        definition.context = new TokenscriptContext();
        definition.context.cAddr = cAddr;
        definition.context.attrInterface = this;

        List<Attribute> attrList = new ArrayList<>(definition.attributes.values());
        if (extraAttrs != null) attrList.addAll(extraAttrs);

        tokenscriptUtility.buildAttrMap(attrList);

        return Observable.fromIterable(attrList)
                .flatMap(attr -> tokenscriptUtility.fetchAttrResult(token, attr, tokenId,
                                                                    definition, this, itemView));
    }

    public Observable<TokenScriptResult.Attribute> resolveAttrs(Token token, List<BigInteger> tokenIds, List<Attribute> extraAttrs)
    {
        TokenDefinition definition = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        //pre-fill tokenIds
        for (Attribute attrType : definition.attributes.values())
        {
            resolveTokenIds(attrType, tokenIds);
        }

        //TODO: store transaction fetch time for multiple tokenIds

        return resolveAttrs(token, tokenIds.get(0), extraAttrs, false);
    }

    private void resolveTokenIds(Attribute attrType, List<BigInteger> tokenIds)
    {
        if (attrType.function == null) return;

        for (MethodArg arg : attrType.function.parameters)
        {
            int index = arg.getTokenIndex();
            if (arg.isTokenId() && index >= 0 && index < tokenIds.size())
            {
                arg.element.value = tokenIds.get(index).toString();
            }
        }
    }

    private List<String> getLocalTSMLFiles()
    {
        List<String> localTSMLFilesStr = new ArrayList<>();
        AssetManager mgr = context.getResources().getAssets();
        try
        {
            String[] filelist = mgr.list("");
            for (String file : filelist)
            {
                if (file.contains("tsml"))
                {
                    localTSMLFilesStr.add(file);
                }
            }

        }
        catch (IOException e)
        {
            e.printStackTrace();
        }

        return localTSMLFilesStr;
    }

    public String generateTransactionPayload(Token token, BigInteger tokenId, FunctionDefinition def)
    {
        TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.tokenInfo.address);
        if (td == null) return "";
        Function function = tokenscriptUtility.generateTransactionFunction(token, tokenId, td, def, this);
        if (function.getInputParameters() == null)
        {
            return null;
        }
        else
        {
            return FunctionEncoder.encode(function);
        }
    }

    /**
     * Clear the currently cached definition. This forces the service to reload the definition so it's clean for the next usage.
     */
    public void clearCache()
    {
        cachedDefinition = null;
    }

    public boolean viewsEqual(Token token)
    {
        String view = getTokenView(token.tokenInfo.chainId, token.tokenInfo.address, ASSET_DETAIL_VIEW_NAME);
        String iconifiedView = getTokenView(token.tokenInfo.chainId, token.tokenInfo.address, ASSET_SUMMARY_VIEW_NAME);
        if (view == null || iconifiedView == null) return false;
        else return view.equals(iconifiedView);
    }

    public List<ContractLocator> getHoldingContracts(TokenDefinition td)
    {
        List<ContractLocator> holdingContracts = new ArrayList<>();
        ContractInfo holdingContractInfo = td.contracts.get(td.holdingToken);
        if (holdingContractInfo == null || holdingContractInfo.addresses.size() == 0) return null;
        for (int chainId : holdingContractInfo.addresses.keySet())
        {
            for (String address : holdingContractInfo.addresses.get(chainId))
            {
                holdingContracts.add(new ContractLocator(address, chainId));
            }
        }

        return holdingContracts;
    }

    public ContractLocator getHoldingContract(String importFileName)
    {
        ContractLocator cr = null;
        for (int i = 0; i < assetDefinitions.size(); i++)
        {
            int chainId = assetDefinitions.keyAt(i);
            for (String address : assetDefinitions.get(chainId).keySet())
            {
                TokenScriptFile f = assetDefinitions.get(chainId).get(address);
                String path = f.getAbsoluteFile().toString();
                if (path.contains(importFileName))
                {
                    cr = new ContractLocator(address, chainId);
                    break;
                }
            }
            if (cr != null) break;
        }

        return cr;
    }

    public String convertInputValue(Attribute attr, String valueFromInput)
    {
        return tokenscriptUtility.convertInputValue(attr, valueFromInput);
    }

    public void setEventCallback(ActionEventCallback callback)
    {
        eventCallback = callback;
        if (requireEventSend)
        {
            requireEventSend = false;
            generateAndSendEvents();
        }
    }

    public String resolveReference(@NotNull Token token, TSAction action, TokenscriptElement arg, BigInteger tokenId)
    {
        TokenDefinition td = getAssetDefinition(token.tokenInfo.chainId, token.getAddress());
        return tokenscriptUtility.resolveReference(token, arg, tokenId, td, this);
    }

    public void setErrorCallback(FragmentMessenger callback)
    {
        homeMessenger = callback;
    }

    /**
     * Using a file search method rather than the pre-parsed method.
     * This lets us catch bad tokenscripts and report on errors.
     *
     * @return List of Tokenscripts with details
     */
    public Single<List<TokenLocator>> getAllTokenDefinitions(boolean refresh)
    {
        return Single.fromCallable(() -> {
            if (refresh)
            {
                loadAssetScripts();
            }
            waitForAssets();
            final File[] files = buildFileList();
            List<TokenLocator> tokenLocators = new ArrayList<>();
            Observable.fromArray(files)
                    .filter(File::isFile)
                    .filter(this::allowableExtension)
                    .filter(File::canRead)
                    .subscribeOn(Schedulers.io())
                    .observeOn(Schedulers.io())
                    .blockingForEach(file -> {
                        try
                        {
                            FileInputStream input = new FileInputStream(file);
                            TokenDefinition tokenDef = parseFile(input);
                            ContractInfo origins = tokenDef.contracts.get(tokenDef.holdingToken);
                            if (origins.addresses.size() > 0)
                            {
                                TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath());
                                tokenLocators.add(new TokenLocator(tokenDef.getTokenName(1), origins, tsf));
                            }
                        } // TODO: Catch specific tokenscript parse errors to report tokenscript errors.
                        catch (Exception e)
                        {
                            TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath());
                            ContractInfo contractInfo = new ContractInfo("Contract Type",new HashMap<>());
                            StringWriter stackTrace = new StringWriter();
                            e.printStackTrace(new PrintWriter(stackTrace));

                            tokenLocators.add(new TokenLocator(file.getName(), contractInfo, tsf, true, stackTrace.toString()));
                        }
                    });

            return tokenLocators;
        });
    }

    private List<ContractLocator> getAllOriginContracts()
    {
        List<ContractLocator> originContracts = new ArrayList<>();
        for (int i = 0; i < assetDefinitions.size(); i++)
        {
            int chainId = assetDefinitions.keyAt(i);
            for (String address : assetDefinitions.get(chainId).keySet())
            {
                if (address.equals("ethereum"))
                    continue;
                if (tokensService.getToken(chainId, address) == null)
                {
                    originContracts.add(new ContractLocator(address, chainId));
                }
            }
        }

        return originContracts;
    }

    public Single<String> checkServerForScript(int chainId, String address)
    {
        TokenScriptFile tf = getTokenScriptFile(chainId, address);
        if (tf != null && !isInSecureZone(tf)) return Single.fromCallable(() -> { return ""; }); //early return for debug script check

        //now try the server
        return fetchXMLFromServer(address.toLowerCase())
                .flatMap(this::cacheSignature)
                .map(this::handleFileLoad)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    public Disposable storeTokenViewHeight(int chainId, String address, int listViewHeight)
    {
        return Completable.complete()
                .subscribeWith(new DisposableCompletableObserver()
                {
                    Realm realm;
                    @Override
                    public void onStart()
                    {
                        TransactionsRealmCache.addRealm();
                        realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress());
                        //determine hash
                        TokenScriptFile tsf = getTokenScriptFile(chainId, address);
                        if (tsf == null || !tsf.exists()) return;
                        String hash = tsf.calcMD5();
                        String databaseKey = tokenSizeDBKey(chainId, address);

                        RealmAuxData realmToken = realm.where(RealmAuxData.class)
                                .equalTo("instanceKey", databaseKey)
                                .equalTo("chainId", chainId)
                                .findFirst();

                        realm.beginTransaction();

                        if (realmToken == null)
                        {
                            realmToken = realm.createObject(RealmAuxData.class, databaseKey);
                        }
                        realmToken.setChainId(chainId);
                        realmToken.setResult(hash);
                        realmToken.setResultTime(listViewHeight);
                    }

                    @Override
                    public void onComplete()
                    {
                        if (realm.isInTransaction()) realm.commitTransaction();
                        TransactionsRealmCache.subRealm();
                        realm.close();
                    }

                    @Override
                    public void onError(Throwable e)
                    {
                        TransactionsRealmCache.subRealm();
                        if (realm != null && !realm.isClosed())
                        {
                            realm.close();
                        }
                    }
                });
    }

    public String getTokenImageUrl(int networkId, String address)
    {
        String url = "";
        String instanceKey = address.toLowerCase() + "-" + networkId;
        try (Realm realm = realmManager.getAuxRealmInstance(IMAGES_DB))
        {
            RealmAuxData instance = realm.where(RealmAuxData.class)
                    .equalTo("instanceKey", instanceKey)
                    .findFirst();

            if (instance != null)
            {
                url = instance.getResult();
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }

        return url;
    }

    public Single<Integer> fetchViewHeight(int chainId, String address)
    {
        return Single.fromCallable(() -> {
            try (Realm realm = realmManager.getAuxRealmInstance(tokensService.getCurrentAddress()))
            {
                //determine hash
                TokenScriptFile tsf = getTokenScriptFile(chainId, address);
                if (tsf == null || !tsf.exists()) return 0;
                String hash = tsf.calcMD5();
                String databaseKey = tokenSizeDBKey(chainId, address);

                RealmAuxData realmToken = realm.where(RealmAuxData.class)
                        .equalTo("instanceKey", databaseKey)
                        .equalTo("chainId", chainId)
                        .findFirst();

                if (realmToken == null)
                {
                    return 0;
                }

                if (hash.equals(realmToken.getResult()))
                {
                    //can use this height
                    return (int)realmToken.getResultTime();
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }

            return 0;
        });
    }

    private String tokenSizeDBKey(int chainId, String address)
    {
        return "szkey-" + chainId + "-" + address.toLowerCase();
    }
}