From 979934b3ccb9a4a684da1f32ede52e8b69810287 Mon Sep 17 00:00:00 2001 From: James Brown Date: Fri, 29 Sep 2023 22:09:51 +1000 Subject: [PATCH] Handle ERC-5169 as per the spec or legacy single string --- app/src/main/AndroidManifest.xml | 7 +++ .../app/entity/ContractInteract.java | 9 ++-- .../app/entity/tokens/Attestation.java | 8 ++-- .../alphawallet/app/entity/tokens/Token.java | 2 +- .../app/repository/TokenRepository.java | 38 ++++++++++++++-- .../app/service/AssetDefinitionService.java | 32 +++++++++----- .../java/com/alphawallet/app/util/Utils.java | 44 +++++++++++++++++++ 7 files changed, 117 insertions(+), 23 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6cee125250..a3fe2c34f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,6 +70,13 @@ + + + + + + + diff --git a/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java b/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java index 5e022ee0a5..1c1bf8bb64 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java +++ b/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java @@ -1,5 +1,6 @@ package com.alphawallet.app.entity; +import static com.alphawallet.app.repository.TokenRepository.callSmartContractFuncAdaptiveArray; import static com.alphawallet.app.repository.TokenRepository.callSmartContractFunction; import android.text.TextUtils; @@ -18,6 +19,7 @@ import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import io.reactivex.Single; @@ -37,12 +39,9 @@ public ContractInteract(Token token) this.token = token; } - public Single getScriptFileURI() + public Single> getScriptFileURI() { - return Single.fromCallable(() -> { - String contractURI = callSmartContractFunction(token.tokenInfo.chainId, getScriptURI(), token.getAddress(), token.getWallet()); - return contractURI != null ? contractURI : ""; - }).observeOn(Schedulers.io()); + return Single.fromCallable(() -> callSmartContractFuncAdaptiveArray(token.tokenInfo.chainId, getScriptURI(), token.getAddress(), token.getWallet())).observeOn(Schedulers.io()); } private String loadMetaData(String tokenURI) diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java index 14a3d44f24..0a100c45cc 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Attestation.java @@ -11,14 +11,12 @@ import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.repository.EthereumNetworkBase; -import com.alphawallet.app.repository.TokensRealmSource; import com.alphawallet.app.repository.entity.RealmAttestation; import com.alphawallet.app.util.Utils; import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.AttestationValidation; import com.alphawallet.token.entity.AttestationValidationStatus; import com.alphawallet.token.entity.TokenScriptResult; -import org.web3j.utils.Numeric; import com.alphawallet.token.tools.TokenDefinition; import com.google.gson.Gson; @@ -30,12 +28,14 @@ import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.crypto.StructuredDataEncoder; +import org.web3j.utils.Numeric; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -901,12 +901,12 @@ public boolean isBytes() } @Override - public Single getScriptURI() + public Single> getScriptURI() { MemberData memberData = additionalMembers.get(SCHEMA_DATA_PREFIX + SCRIPT_URI); if (memberData != null && !TextUtils.isEmpty(memberData.getString())) { - return Single.fromCallable(memberData::getString); + return Single.fromCallable(() -> Collections.singletonList(memberData.getString())); } else { diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java index 5618d16c46..7033d5f4db 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/Token.java @@ -1078,7 +1078,7 @@ public Single> buildAssetList(TokenTransferData transferData) }); } - public Single getScriptURI() + public Single> getScriptURI() { return contractInteract.getScriptFileURI(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java index 538893dc1d..5eb3d6951a 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -3,6 +3,7 @@ import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; import static com.alphawallet.ethereum.EthereumNetworkBase.OKX_ID; import static org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction; +import static java.util.Arrays.asList; import android.content.Context; import android.text.TextUtils; @@ -1058,7 +1059,7 @@ private String callCustomNetSmartContractFunction( } public static byte[] createTokenTransferData(String to, BigInteger tokenAmount) { - List params = Arrays.asList(new Address(to), new Uint256(tokenAmount)); + List params = asList(new Address(to), new Uint256(tokenAmount)); List> returnTypes = Collections.singletonList(new TypeReference() {}); Function function = new Function("transfer", params, returnTypes); String encodedFunction = FunctionEncoder.encode(function); @@ -1082,7 +1083,7 @@ public static byte[] createERC721TransferFunction(String to, Token token, List> returnTypes = Collections.emptyList(); - List params = Arrays.asList(new Address(from), new Address(to), new Uint256(tokenId)); + List params = asList(new Address(from), new Address(to), new Uint256(tokenId)); Function function = new Function("safeTransferFrom", params, returnTypes); String encodedFunction = FunctionEncoder.encode(function); @@ -1107,7 +1108,7 @@ public static byte[] createDropCurrency(MagicLinkData order, int v, byte[] r, by { Function function = new Function( "dropCurrency", - Arrays.asList(new org.web3j.abi.datatypes.generated.Uint32(order.nonce), + asList(new org.web3j.abi.datatypes.generated.Uint32(order.nonce), new org.web3j.abi.datatypes.generated.Uint32(order.amount), new org.web3j.abi.datatypes.generated.Uint32(order.expiry), new org.web3j.abi.datatypes.generated.Uint8(v), @@ -1356,6 +1357,37 @@ public static String callSmartContractFunction(long chainId, return null; } + public static List callSmartContractFuncAdaptiveArray(long chainId, + Function function, String contractAddress, String walletAddr) + { + String encodedFunction = FunctionEncoder.encode(function); + + try + { + org.web3j.protocol.core.methods.request.Transaction transaction + = createEthCallTransaction(walletAddr, contractAddress, encodedFunction); + EthCall response = getWeb3jService(chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send(); + + List responseValues = FunctionReturnDecoder.decode(response.getValue(), function.getOutputParameters()); + List responseValuesArray = Utils.decodeDynamicArray(response.getValue()); + + if (!responseValuesArray.isEmpty()) // if arrays are found, return these as the filter is more strict + { + return (List)Utils.asAList(responseValuesArray, " "); + } + if (!responseValues.isEmpty()) + { + return Collections.singletonList(responseValues.get(0).getValue().toString()); + } + } + catch (Exception e) + { + // + } + + return new ArrayList<>(); + } + public static List callSmartContractFunctionArray(long chainId, Function function, String contractAddress, String walletAddr) { diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index ce3a0c69b4..a7f58663bb 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -1262,17 +1262,30 @@ private void updateScriptEntriesInRealm(List origins, boolean i //Call contract and check for script private Single fetchTokenScriptFromContract(Token token, MutableLiveData updateFlag) { + //Allow for arrays of URI, check each in turn for multiple and return the first valid entry return token.getScriptURI() - .map(uri -> { - if (!TextUtils.isEmpty(uri) && updateFlag != null) + .map(uriList -> { + for (String uri : uriList) { - updateFlag.postValue(true); + //early return for unchanged IPFS + if (matchesExistingScript(token, uri)) + { + break; // return script unchanged / not found + } + //download each in turn, return first valid script + if (!TextUtils.isEmpty(uri) && updateFlag != null) + { + updateFlag.postValue(true); + } + Pair scriptCandidate = downloadScript(uri, 0); + if (!TextUtils.isEmpty(scriptCandidate.first)) + { + return new Pair<>(uri, scriptCandidate); + } } - return uri; + return new Pair<>("", new Pair<>("", false)); }) - .map(uri -> compareExistingScript(token, uri)) - .map(uri -> new Pair<>(uri, downloadScript(uri, 0))) .map(scriptData -> storeEntry(token, scriptData)); } @@ -1341,10 +1354,9 @@ private File storeEntry(Token token, Pair> scriptD return storeFile; } - private String compareExistingScript(Token token, String uri) + private boolean matchesExistingScript(Token token, String uri) { //TODO: calculate and use the IPFS CID to validate existing script against IPFS locator - String returnUri = uri; try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) { String entryKey = token.getTSKey(); //getTSDataKey(token.tokenInfo.chainId, token.tokenInfo.address); @@ -1361,7 +1373,7 @@ private String compareExistingScript(Token token, String uri) && entry.getIpfsPath().equals(uri) && tsf.exists()) { - returnUri = UNCHANGED_SCRIPT; + return true; } } catch (Exception e) @@ -1369,7 +1381,7 @@ private String compareExistingScript(Token token, String uri) Timber.w(e); } - return returnUri; + return false; } private Single tryServerIfRequired(File contractScript, String address) diff --git a/app/src/main/java/com/alphawallet/app/util/Utils.java b/app/src/main/java/com/alphawallet/app/util/Utils.java index 696b2e7917..186ac955d9 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -46,6 +46,11 @@ import org.jetbrains.annotations.NotNull; import org.json.JSONObject; +import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.DynamicArray; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; import org.web3j.crypto.Hash; import org.web3j.crypto.Keys; import org.web3j.crypto.StructuredDataEncoder; @@ -71,6 +76,7 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -1071,6 +1077,44 @@ public static String calculateContractAddress(String account, long nonce) return Keys.toChecksumAddress(Numeric.toHexString(calculatedAddressAsBytes)); } + public static List decodeDynamicArray(String output) + { + List> adaptive = org.web3j.abi.Utils.convert(Collections.singletonList(new TypeReference>() {})); + try + { + return FunctionReturnDecoder.decode(output, adaptive); + } + catch (Exception e) + { + // Expected + } + + return new ArrayList<>(); + } + + public static List asAList(List responseValues, T convert) + { + List converted = new ArrayList<>(); + if (responseValues.isEmpty()) + { + return converted; + } + + for (Object objUri : ((DynamicArray) responseValues.get(0)).getValue()) + { + try + { + converted.add((T) ((Type) objUri).getValue().toString()); + } + catch (ClassCastException e) + { + // + } + } + + return converted; + } + public static boolean isJson(String value) { try