package com.alphawallet.token.web.Ethereum; import io.reactivex.Observable; import com.alphawallet.token.entity.*; import com.alphawallet.token.tools.TokenDefinition; import com.alphawallet.token.web.Service.EthRPCNodes; import okhttp3.OkHttpClient; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.FunctionReturnDecoder; import org.web3j.abi.TypeReference; import org.web3j.abi.datatypes.Address; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Int; import org.web3j.abi.datatypes.Type; import org.web3j.abi.datatypes.Uint; import org.web3j.abi.datatypes.Utf8String; import org.web3j.abi.datatypes.generated.Bytes1; import org.web3j.abi.datatypes.generated.Bytes10; import org.web3j.abi.datatypes.generated.Bytes11; import org.web3j.abi.datatypes.generated.Bytes12; import org.web3j.abi.datatypes.generated.Bytes13; import org.web3j.abi.datatypes.generated.Bytes14; import org.web3j.abi.datatypes.generated.Bytes15; import org.web3j.abi.datatypes.generated.Bytes16; import org.web3j.abi.datatypes.generated.Bytes17; import org.web3j.abi.datatypes.generated.Bytes18; import org.web3j.abi.datatypes.generated.Bytes19; import org.web3j.abi.datatypes.generated.Bytes2; import org.web3j.abi.datatypes.generated.Bytes20; import org.web3j.abi.datatypes.generated.Bytes21; import org.web3j.abi.datatypes.generated.Bytes22; import org.web3j.abi.datatypes.generated.Bytes23; import org.web3j.abi.datatypes.generated.Bytes24; import org.web3j.abi.datatypes.generated.Bytes25; import org.web3j.abi.datatypes.generated.Bytes26; import org.web3j.abi.datatypes.generated.Bytes27; import org.web3j.abi.datatypes.generated.Bytes28; import org.web3j.abi.datatypes.generated.Bytes29; import org.web3j.abi.datatypes.generated.Bytes3; import org.web3j.abi.datatypes.generated.Bytes30; import org.web3j.abi.datatypes.generated.Bytes31; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Bytes4; import org.web3j.abi.datatypes.generated.Bytes5; import org.web3j.abi.datatypes.generated.Bytes6; import org.web3j.abi.datatypes.generated.Bytes7; import org.web3j.abi.datatypes.generated.Bytes8; import org.web3j.abi.datatypes.generated.Bytes9; import org.web3j.abi.datatypes.generated.Int104; import org.web3j.abi.datatypes.generated.Int112; import org.web3j.abi.datatypes.generated.Int120; import org.web3j.abi.datatypes.generated.Int128; import org.web3j.abi.datatypes.generated.Int136; import org.web3j.abi.datatypes.generated.Int144; import org.web3j.abi.datatypes.generated.Int152; import org.web3j.abi.datatypes.generated.Int16; import org.web3j.abi.datatypes.generated.Int160; import org.web3j.abi.datatypes.generated.Int168; import org.web3j.abi.datatypes.generated.Int176; import org.web3j.abi.datatypes.generated.Int184; import org.web3j.abi.datatypes.generated.Int192; import org.web3j.abi.datatypes.generated.Int200; import org.web3j.abi.datatypes.generated.Int208; import org.web3j.abi.datatypes.generated.Int216; import org.web3j.abi.datatypes.generated.Int224; import org.web3j.abi.datatypes.generated.Int232; import org.web3j.abi.datatypes.generated.Int24; import org.web3j.abi.datatypes.generated.Int240; import org.web3j.abi.datatypes.generated.Int248; import org.web3j.abi.datatypes.generated.Int256; import org.web3j.abi.datatypes.generated.Int32; import org.web3j.abi.datatypes.generated.Int40; import org.web3j.abi.datatypes.generated.Int48; import org.web3j.abi.datatypes.generated.Int56; import org.web3j.abi.datatypes.generated.Int64; import org.web3j.abi.datatypes.generated.Int72; import org.web3j.abi.datatypes.generated.Int8; import org.web3j.abi.datatypes.generated.Int80; import org.web3j.abi.datatypes.generated.Int88; import org.web3j.abi.datatypes.generated.Int96; import org.web3j.abi.datatypes.generated.Uint104; import org.web3j.abi.datatypes.generated.Uint112; import org.web3j.abi.datatypes.generated.Uint120; import org.web3j.abi.datatypes.generated.Uint128; import org.web3j.abi.datatypes.generated.Uint136; import org.web3j.abi.datatypes.generated.Uint144; import org.web3j.abi.datatypes.generated.Uint152; import org.web3j.abi.datatypes.generated.Uint16; import org.web3j.abi.datatypes.generated.Uint160; import org.web3j.abi.datatypes.generated.Uint168; import org.web3j.abi.datatypes.generated.Uint176; import org.web3j.abi.datatypes.generated.Uint184; import org.web3j.abi.datatypes.generated.Uint192; import org.web3j.abi.datatypes.generated.Uint200; import org.web3j.abi.datatypes.generated.Uint208; import org.web3j.abi.datatypes.generated.Uint216; import org.web3j.abi.datatypes.generated.Uint224; import org.web3j.abi.datatypes.generated.Uint232; import org.web3j.abi.datatypes.generated.Uint24; import org.web3j.abi.datatypes.generated.Uint240; import org.web3j.abi.datatypes.generated.Uint248; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.abi.datatypes.generated.Uint32; import org.web3j.abi.datatypes.generated.Uint40; import org.web3j.abi.datatypes.generated.Uint48; import org.web3j.abi.datatypes.generated.Uint56; import org.web3j.abi.datatypes.generated.Uint64; import org.web3j.abi.datatypes.generated.Uint72; import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.abi.datatypes.generated.Uint80; import org.web3j.abi.datatypes.generated.Uint88; import org.web3j.abi.datatypes.generated.Uint96; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.DefaultBlockParameterName; import org.web3j.protocol.core.methods.response.EthCall; import org.web3j.protocol.http.HttpService; import org.web3j.utils.Bytes; import org.web3j.utils.Numeric; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; /** * Created by James on 13/06/2019. * Stormbird in Sydney */ public abstract class TokenscriptFunction { public static final String TOKENSCRIPT_CONVERSION_ERROR = "<error>"; private final Map<String, Attribute> localAttrs = new ConcurrentHashMap<>(); private final Map<String, String> refTags = new ConcurrentHashMap<>(); public Function generateTransactionFunction(String walletAddr, BigInteger tokenId, TokenDefinition definition, FunctionDefinition function, AttributeInterface attrIf) { boolean valueNotFound = false; //pre-parse tokenId. if (tokenId.bitCount() > 256) tokenId = tokenId.or(BigInteger.ONE.shiftLeft(256).subtract(BigInteger.ONE)); //truncate tokenId too large List<Type> params = new ArrayList<Type>(); List<TypeReference<?>> returnTypes = new ArrayList<TypeReference<?>>(); for (MethodArg arg : function.parameters) { String value = resolveReference(walletAddr, arg.element, tokenId, definition, attrIf); //get arg.element.value in the form of BigInteger if appropriate byte[] argValueBytes = null; BigInteger argValueBI = null; if (valueNotFound) { params = null; continue; } if (value != null && !arg.parameterType.equals("string")) { argValueBytes = convertArgToBytes(value); argValueBI = new BigInteger(1, argValueBytes); } try { switch (arg.parameterType) { case "int": params.add(new Int(argValueBI)); break; case "int8": params.add(new Int8(argValueBI)); break; case "int16": params.add(new Int16(argValueBI)); break; case "int24": params.add(new Int24(argValueBI)); break; case "int32": params.add(new Int32(argValueBI)); break; case "int40": params.add(new Int40(argValueBI)); break; case "int48": params.add(new Int48(argValueBI)); break; case "int56": params.add(new Int56(argValueBI)); break; case "int64": params.add(new Int64(argValueBI)); break; case "int72": params.add(new Int72(argValueBI)); break; case "int80": params.add(new Int80(argValueBI)); break; case "int88": params.add(new Int88(argValueBI)); break; case "int96": params.add(new Int96(argValueBI)); break; case "int104": params.add(new Int104(argValueBI)); break; case "int112": params.add(new Int112(argValueBI)); break; case "int120": params.add(new Int120(argValueBI)); break; case "int128": params.add(new Int128(argValueBI)); break; case "int136": params.add(new Int136(argValueBI)); break; case "int144": params.add(new Int144(argValueBI)); break; case "int152": params.add(new Int152(argValueBI)); break; case "int160": params.add(new Int160(argValueBI)); break; case "int168": params.add(new Int168(argValueBI)); break; case "int176": params.add(new Int176(argValueBI)); break; case "int184": params.add(new Int184(argValueBI)); break; case "int192": params.add(new Int192(argValueBI)); break; case "int200": params.add(new Int200(argValueBI)); break; case "int208": params.add(new Int208(argValueBI)); break; case "int216": params.add(new Int216(argValueBI)); break; case "int224": params.add(new Int224(argValueBI)); break; case "int232": params.add(new Int232(argValueBI)); break; case "int240": params.add(new Int240(argValueBI)); break; case "int248": params.add(new Int248(argValueBI)); break; case "int256": params.add(new Int256(argValueBI)); break; case "uint": params.add(new Uint(argValueBI)); break; case "uint8": params.add(new Uint8(argValueBI)); break; case "uint16": params.add(new Uint16(argValueBI)); break; case "uint24": params.add(new Uint24(argValueBI)); break; case "uint32": params.add(new Uint32(argValueBI)); break; case "uint40": params.add(new Uint40(argValueBI)); break; case "uint48": params.add(new Uint48(argValueBI)); break; case "uint56": params.add(new Uint56(argValueBI)); break; case "uint64": params.add(new Uint64(argValueBI)); break; case "uint72": params.add(new Uint72(argValueBI)); break; case "uint80": params.add(new Uint80(argValueBI)); break; case "uint88": params.add(new Uint88(argValueBI)); break; case "uint96": params.add(new Uint96(argValueBI)); break; case "uint104": params.add(new Uint104(argValueBI)); break; case "uint112": params.add(new Uint112(argValueBI)); break; case "uint120": params.add(new Uint120(argValueBI)); break; case "uint128": params.add(new Uint128(argValueBI)); break; case "uint136": params.add(new Uint136(argValueBI)); break; case "uint144": params.add(new Uint144(argValueBI)); break; case "uint152": params.add(new Uint152(argValueBI)); break; case "uint160": params.add(new Uint160(argValueBI)); break; case "uint168": params.add(new Uint168(argValueBI)); break; case "uint176": params.add(new Uint176(argValueBI)); break; case "uint184": params.add(new Uint184(argValueBI)); break; case "uint192": params.add(new Uint192(argValueBI)); break; case "uint200": params.add(new Uint200(argValueBI)); break; case "uint208": params.add(new Uint208(argValueBI)); break; case "uint216": params.add(new Uint216(argValueBI)); break; case "uint224": params.add(new Uint224(argValueBI)); break; case "uint232": params.add(new Uint232(argValueBI)); break; case "uint240": params.add(new Uint240(argValueBI)); break; case "uint248": params.add(new Uint248(argValueBI)); break; case "uint256": switch (arg.element.ref) { case "tokenId": params.add(new Uint256(tokenId)); break; case "value": default: params.add(new Uint256(argValueBI)); break; } break; case "address": switch (arg.element.ref) { case "ownerAddress": params.add(new Address(walletAddr)); break; case "value": default: params.add(new Address(Numeric.toHexString(argValueBytes))); break; } break; case "string": if (value == null) throw new Exception("Attempt to use null value"); params.add(new Utf8String(value)); break; case "bytes": if (value == null) throw new Exception("Attempt to use null value"); params.add(new Bytes32(Numeric.hexStringToByteArray(value))); break; case "bytes1": params.add(new Bytes1(argValueBytes)); break; case "bytes2": params.add(new Bytes2(argValueBytes)); break; case "bytes3": params.add(new Bytes3(argValueBytes)); break; case "bytes4": params.add(new Bytes4(argValueBytes)); break; case "bytes5": params.add(new Bytes5(argValueBytes)); break; case "bytes6": params.add(new Bytes6(argValueBytes)); break; case "bytes7": params.add(new Bytes7(argValueBytes)); break; case "bytes8": params.add(new Bytes8(argValueBytes)); break; case "bytes9": params.add(new Bytes9(argValueBytes)); break; case "bytes10": params.add(new Bytes10(argValueBytes)); break; case "bytes11": params.add(new Bytes11(argValueBytes)); break; case "bytes12": params.add(new Bytes12(argValueBytes)); break; case "bytes13": params.add(new Bytes13(argValueBytes)); break; case "bytes14": params.add(new Bytes14(argValueBytes)); break; case "bytes15": params.add(new Bytes15(argValueBytes)); break; case "bytes16": params.add(new Bytes16(argValueBytes)); break; case "bytes17": params.add(new Bytes17(argValueBytes)); break; case "bytes18": params.add(new Bytes18(argValueBytes)); break; case "bytes19": params.add(new Bytes19(argValueBytes)); break; case "bytes20": params.add(new Bytes20(argValueBytes)); break; case "bytes21": params.add(new Bytes21(argValueBytes)); break; case "bytes22": params.add(new Bytes22(argValueBytes)); break; case "bytes23": params.add(new Bytes23(argValueBytes)); break; case "bytes24": params.add(new Bytes24(argValueBytes)); break; case "bytes25": params.add(new Bytes25(argValueBytes)); break; case "bytes26": params.add(new Bytes26(argValueBytes)); break; case "bytes27": params.add(new Bytes27(argValueBytes)); break; case "bytes28": params.add(new Bytes28(argValueBytes)); break; case "bytes29": params.add(new Bytes29(argValueBytes)); break; case "bytes30": params.add(new Bytes30(argValueBytes)); break; case "bytes31": params.add(new Bytes31(argValueBytes)); break; case "bytes32": //sometimes tokenId can be passed as bytes32 switch (arg.element.ref) { case "tokenId": params.add(new Bytes32(Numeric.toBytesPadded(tokenId, 32))); break; case "value": params.add(new Bytes32(argValueBytes)); break; default: params.add(new Bytes32(Numeric.toBytesPadded(argValueBI, 32))); break; } break; default: System.out.println("NOT IMPLEMENTED: " + arg.parameterType); break; } } catch (Exception e) { //attempting to use unformed value valueNotFound = true; } } switch (function.as) { case UTF8: returnTypes.add(new TypeReference<Utf8String>() {}); break; case Signed: case Unsigned: case UnsignedInput: case TokenId: returnTypes.add(new TypeReference<Uint256>() {}); break; case Address: returnTypes.add(new TypeReference<Address>() {}); break; case Mapping: case Boolean: default: returnTypes.add(new TypeReference<Bytes32>() {}); break; } if (valueNotFound) { params = null; } return new Function(function.method, params, returnTypes); } public static byte[] convertArgToBytes(String inputValue) { byte[] argBytes = new byte[1]; try { String hexValue = inputValue; if (!Numeric.containsHexPrefix(inputValue)) { BigInteger value; try { value = new BigInteger(inputValue); } catch (NumberFormatException e) { value = new BigInteger(inputValue, 16); } hexValue = Numeric.toHexStringNoPrefix(value.toByteArray()); //fix sign condition if (hexValue.length() > 64 && hexValue.startsWith("00")) { hexValue = hexValue.substring(2); } } argBytes = Numeric.hexStringToByteArray(hexValue); } catch (Exception e) { //no action } return argBytes; } private String handleTransactionResult(TransactionResult result, Function function, String responseValue, Attribute attr, long lastTransactionTime) { String transResult = null; try { //try to interpret the value. For now, just use the raw return value - this is more reliable until we need to interpret arrays List<Type> response = FunctionReturnDecoder.decode(responseValue, function.getOutputParameters()); if (response.size() > 0) { result.resultTime = lastTransactionTime; Type val = response.get(0); BigInteger value; byte[] bytes = Bytes.trimLeadingZeroes(Numeric.hexStringToByteArray(responseValue)); String hexBytes = Numeric.toHexString(bytes); switch (attr.syntax) { case Boolean: value = Numeric.toBigInt(hexBytes); transResult = value.equals(BigDecimal.ZERO) ? "FALSE" : "TRUE"; break; case Integer: value = Numeric.toBigInt(hexBytes); transResult = value.toString(); break; case BitString: case NumericString: if (val.getTypeAsString().equals("string")) { transResult = (String)val.getValue(); if (responseValue.length() > 2 && transResult.length() == 0) { transResult = checkBytesString(responseValue); } } else { //should be a decimal string value = Numeric.toBigInt(hexBytes); transResult = value.toString(); } break; case IA5String: case DirectoryString: case GeneralizedTime: case CountryString: if (val.getTypeAsString().equals("string")) { transResult = (String)val.getValue(); if (responseValue.length() > 2 && transResult.length() == 0) { transResult = checkBytesString(responseValue); } } else if (val.getTypeAsString().equals("address")) { transResult = (String)val.getValue(); } else { transResult = hexBytes; } break; default: transResult = hexBytes; break; } } else { result.resultTime = lastTransactionTime == -1 ? -1 : 0; } } catch (Exception e) { e.printStackTrace(); } return transResult; } private String checkBytesString(String responseValue) throws Exception { String name = ""; if (responseValue.length() > 0) { //try raw bytes byte[] data = Numeric.hexStringToByteArray(responseValue); //check leading bytes for non-zero if (data[0] != 0) { //truncate zeros int index = data.length - 1; while (data[index] == 0 && index > 0) index--; if (index != (data.length - 1)) { data = Arrays.copyOfRange(data, 0, index + 1); } name = new String(data, "UTF-8"); } } return name; } public static final String ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; /** * Haven't pre-cached this value yet, so need to fetch it before we can proceed * @param attr * @param tokenId * @param definition * @return */ public Observable<TransactionResult> fetchResultFromEthereum(String walletAddress, ContractAddress contractAddress, Attribute attr, BigInteger tokenId, TokenDefinition definition, AttributeInterface attrIf, long lastTransactionTime) { return Observable.fromCallable(() -> { long txUpdateTime = lastTransactionTime; TransactionResult transactionResult = new TransactionResult(contractAddress.chainId, contractAddress.address, tokenId, attr); // 1: create transaction call org.web3j.abi.datatypes.Function transaction = generateTransactionFunction(walletAddress, tokenId, definition, attr.function, attrIf); // 2: create web3 connection OkHttpClient okClient = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .build(); HttpService nodeService = new HttpService(EthRPCNodes.getNodeURLByNetworkId(contractAddress.chainId), okClient, false); Web3j web3j = Web3j.build(nodeService); //now push the transaction String result; if (transaction.getInputParameters() == null) { //couldn't validate all the input param values result = ""; txUpdateTime = -1; } else { //now push the transaction result = callSmartContractFunction(web3j, transaction, contractAddress.address, ZERO_ADDRESS); } transactionResult.result = handleTransactionResult(transactionResult, transaction, result, attr, txUpdateTime); return transactionResult; }); } private String callSmartContractFunction(Web3j web3j, Function function, String contractAddress, String walletAddr) throws Exception { String encodedFunction = FunctionEncoder.encode(function); try { org.web3j.protocol.core.methods.request.Transaction transaction = createEthCallTransaction(walletAddr, contractAddress, encodedFunction); EthCall response = web3j.ethCall(transaction, DefaultBlockParameterName.LATEST).send(); return response.getValue(); } catch (IOException e) { //Connection error. Use cached value return null; } catch (Exception e) { e.printStackTrace(); return null; } } public TokenScriptResult.Attribute parseFunctionResult(TransactionResult transactionResult, Attribute attr) { String res = attr.getSyntaxVal(transactionResult.result); BigInteger val = transactionResult.tokenId; //? if (attr.syntax == TokenDefinition.Syntax.Boolean) { if (res.equalsIgnoreCase("TRUE")) val = BigInteger.ONE; else val = BigInteger.ZERO; } else if (attr.syntax == TokenDefinition.Syntax.NumericString && attr.as != As.Address) { if (transactionResult.result == null) { res = "0"; } else if (transactionResult.result.startsWith("0x")) { res = res.substring(2); } try { val = new BigInteger(res, 16); } catch (NumberFormatException e) { val = BigInteger.ZERO; } } return new TokenScriptResult.Attribute(attr.name, attr.label, val, res); } private void resolveReference(String walletAddress, MethodArg arg, BigInteger tokenId, TokenDefinition definition, AttributeInterface attrIf) { if (definition != null && definition.attributes.containsKey(arg.element.ref)) { arg.element.value = fetchAttrResult(walletAddress, definition.attributes.get(arg.element.ref), tokenId, definition, attrIf).blockingSingle().text; } } public String resolveReference(String walletAddress, TokenscriptElement element, BigInteger tokenId, TokenDefinition definition, AttributeInterface attrIf) { if (isEmpty(element.value)) { return element.value; } if (definition != null && definition.attributes.containsKey(element.ref)) //resolve from attribute { Attribute attr = definition.attributes.get(element.ref); return fetchArgValue(walletAddress, element, attr, tokenId, definition, attrIf); } else if (localAttrs.containsKey(element.ref)) //wasn't able to resolve, attempt to resolve from local attributes or mark null if unresolved user input { Attribute attr = localAttrs.get(element.ref); return fetchArgValue(walletAddress, element, attr, tokenId, definition, attrIf); } else if (localAttrs.containsKey(element.localRef)) { Attribute attr = localAttrs.get(element.localRef); return fetchArgValue(walletAddress, element, attr, tokenId, definition, attrIf); } else if (!isEmpty(element.localRef) && refTags.containsKey(element.localRef)) { return refTags.get(element.localRef); } else { return null; } } private String fetchArgValue(String walletAddress, TokenscriptElement element, Attribute attr, BigInteger tokenId, TokenDefinition definition, AttributeInterface attrIf) { if (attr.userInput) { if (!isEmpty(element.value)) return element.value; //nullify user input if value is not set } else if (!isEmpty(element.value)) { return element.value; } else { return fetchAttrResult(walletAddress, attr, tokenId, definition, attrIf).blockingSingle().text; } return null; } private boolean isEmpty(String str) { return str == null || str.length() == 0; } /** * Fetches a TokenScript attribute * * May either return a static attribute sourced from the Token ID or a dynamic one from a contract function * * If a dynamic function, then test to see if we can source the value from the result cache - * Test to see if the contract has seen any transactions AFTER the result was cached, if so then invalidate the result and re-fetch * * TODO: there is currently an optimisation issue with this. If the contract being called to resolve the attribute is not the token * contract which the script refers to then the result is not cached. This can be seen eg with 'ENABLE' functions which permit * the script contract to manipulate the tokens which the 'ENABLE' function is being called on. * It may not be possible to always safely cache these values; even with an event handler we have to interpret those events and invalidate * any cached results. However if we're tracking the referenced contract as a token then it should be safe * * @param walletAddress * @param attr * @param tokenId * @param td * @param attrIf * @return */ public Observable<TokenScriptResult.Attribute> fetchAttrResult(String walletAddress, Attribute attr, BigInteger tokenId, TokenDefinition td, AttributeInterface attrIf) { if (attr == null) { return Observable.fromCallable(() -> new TokenScriptResult.Attribute("bd", "bd", BigInteger.ZERO, "")); } else if (attr.event != null) { //retrieve events from DB ContractAddress useAddress = new ContractAddress(attr.event.contract.addresses.keySet().iterator().next(), attr.event.contract.addresses.values().iterator().next().get(0)); TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds return resultFromDatabase(cachedResult, attr); } else if (attr.function == null) // static attribute from tokenId (eg city mapping from tokenId) { return staticAttribute(attr, tokenId); } else { ContractAddress useAddress = new ContractAddress(attr.function); //always use the function attribute's address long lastTxUpdate = attrIf.getLastTokenUpdate(useAddress.chainId, useAddress.address); TransactionResult cachedResult = attrIf.getFunctionResult(useAddress, attr, tokenId); //Needs to allow for multiple tokenIds if (cachedResult.resultTime > 0 && ((!attr.isVolatile() && ((attrIf.resolveOptimisedAttr(useAddress, attr, cachedResult) || !cachedResult.needsUpdating(lastTxUpdate)))))) //can we use wallet's known data or cached value? { return resultFromDatabase(cachedResult, attr); } else //if cached value is invalid or if value is dynamic { //for function query, never need wallet address return fetchResultFromEthereum(walletAddress, useAddress, attr, tokenId, td, attrIf, 0) // Fetch function result from blockchain .map(result -> restoreFromDBIfRequired(result, cachedResult)) // If network unavailable restore value from cache .map(attrIf::storeAuxData) // store new data .map(result -> parseFunctionResult(result, attr)); // write } } } public Observable<TokenScriptResult.Attribute> resolveAttributes(String walletAddress, BigInteger tokenId, AttributeInterface attrIf, ContractAddress cAddr, TokenDefinition td) { td.context = new TokenscriptContext(); td.context.cAddr = cAddr; td.context.attrInterface = attrIf; return Observable.fromIterable(new ArrayList<>(td.attributes.values())) .flatMap(attr -> fetchAttrResult(walletAddress, attr, tokenId, td, attrIf)); } private Observable<TokenScriptResult.Attribute> staticAttribute(Attribute attr, BigInteger tokenId) { return Observable.fromCallable(() -> { try { BigInteger val = tokenId.and(attr.bitmask).shiftRight(attr.bitshift); return new TokenScriptResult.Attribute(attr.name, attr.label, val, attr.getSyntaxVal(attr.toString(val))); } catch (Exception e) { return new TokenScriptResult.Attribute(attr.name, attr.label, tokenId, "unsupported encoding"); } }); } private Observable<TokenScriptResult.Attribute> resultFromDatabase(TransactionResult transactionResult, Attribute attr) { return Observable.fromCallable(() -> parseFunctionResult(transactionResult, attr)); } /** * Restore result from Database if required (eg connection failure), and if there was a database value to restore * @param result * @param transactionResult * @return */ private TransactionResult restoreFromDBIfRequired(TransactionResult result, TransactionResult transactionResult) { if (result.resultTime == 0 && transactionResult != null) { result.result = transactionResult.result; result.resultTime = transactionResult.resultTime; } return result; } }