From ea5bc7f6316bc1472d36d285e3cca0cc8c123053 Mon Sep 17 00:00:00 2001 From: James Brown Date: Sun, 27 Aug 2023 23:42:41 +1000 Subject: [PATCH] Add Smartpass handling (#3275) * Update Android deeplink debug key * Update code to add smartpass and handle TokenScript for EAS Attestations * Update controller * Update libraries & token --- app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 361 ++++---- app/src/main/cpp/keys.c | 22 + .../app/di/RepositoriesModule.java | 2 - .../app/entity/ContractInteract.java | 11 +- .../app/entity/EasAttestation.java | 89 +- .../alphawallet/app/entity/SuggestEIP1559.kt | 3 +- .../app/entity/TransactionDecoder.java | 4 +- .../app/entity/TransactionInput.java | 2 +- .../entity/attestation/AttestationImport.java | 768 ++++++++++++++++++ .../AttestationImportInterface.java | 10 + .../entity/attestation/SmartPassReturn.java | 21 + .../app/entity/nftassets/NFTAsset.java | 1 - .../app/entity/tokens/Attestation.java | 143 +++- .../app/entity/tokens/ERC1155Token.java | 4 +- .../alphawallet/app/entity/tokens/Token.java | 38 +- .../app/entity/tokens/TokenFactory.java | 54 ++ .../app/entity/tokenscript/EventUtils.java | 10 +- .../interact/FindDefaultNetworkInteract.java | 1 + .../app/repository/EthereumNetworkBase.java | 6 + .../app/repository/KeyProvider.java | 4 + .../app/repository/KeyProviderJNIImpl.java | 4 + .../app/repository/TokenRepository.java | 29 +- .../app/repository/TokensRealmSource.java | 48 +- .../repository/entity/RealmAttestation.java | 15 +- .../app/repository/entity/RealmToken.java | 75 +- .../app/service/AWHttpService.java | 6 +- .../app/service/AssetDefinitionService.java | 334 +------- .../app/service/BlockNativeGasAPI.java | 8 - .../alphawallet/app/service/GasService.java | 38 +- .../app/service/KeystoreAccountService.java | 4 +- .../app/service/SignatureLookupService.java | 5 +- .../alphawallet/app/ui/AddTokenActivity.java | 4 +- .../app/ui/DappBrowserFragment.java | 2 + .../alphawallet/app/ui/FunctionActivity.java | 58 +- .../com/alphawallet/app/ui/HomeActivity.java | 120 ++- .../app/ui/ImportWalletActivity.java | 2 +- .../alphawallet/app/ui/MyAddressActivity.java | 2 +- .../app/ui/NFTAssetDetailActivity.java | 36 +- .../app/ui/WalletConnectV2Activity.java | 5 + .../alphawallet/app/ui/WalletFragment.java | 41 +- .../app/ui/widget/adapter/ChainAdapter.java | 15 +- .../app/ui/widget/holder/TokenHolder.java | 10 +- .../com/alphawallet/app/util/QRParser.java | 6 +- .../java/com/alphawallet/app/util/Utils.java | 188 +++-- .../app/viewmodel/CustomNetworkViewModel.java | 2 +- .../app/viewmodel/HomeViewModel.java | 74 +- .../app/viewmodel/MyAddressViewModel.java | 4 - .../viewmodel/NetworkChooserViewModel.java | 11 +- .../app/viewmodel/NetworkToggleViewModel.java | 5 - .../app/viewmodel/TokenFunctionViewModel.java | 10 - .../app/viewmodel/WalletConnectViewModel.java | 1 + .../app/viewmodel/WalletViewModel.java | 207 ----- .../app/viewmodel/WalletsViewModel.java | 3 +- .../walletconnect/AWWalletConnectClient.java | 5 + .../TransactionDialogBuilder.java | 9 + .../alphawallet/app/web3/entity/Address.java | 4 +- .../alphawallet/app/widget/NFTImageView.java | 18 + .../com/alphawallet/app/widget/TokenIcon.java | 13 +- .../app/widget/TransactionDetailWidget.java | 4 +- app/src/main/res/drawable/smart_pass.xml | 25 + .../res/layout/activity_nft_asset_detail.xml | 7 + app/src/main/res/values-es/strings.xml | 7 + app/src/main/res/values-fr/strings.xml | 7 + app/src/main/res/values-id/strings.xml | 7 + app/src/main/res/values-my/strings.xml | 7 + app/src/main/res/values-vi/strings.xml | 7 + app/src/main/res/values-zh/strings.xml | 7 + app/src/main/res/values/strings.xml | 7 + .../com/alphawallet/app/QRSelectionTest.java | 5 +- .../alphawallet/app/UniversalLinkTest.java | 2 +- .../app/UniversalLinkTypeTest.java | 3 +- .../app/di/mock/KeyProviderMockImpl.java | 12 + .../KeyProviderMockNonProductionImpl.java | 12 + .../app/viewmodel/HomeViewModelTest.java | 2 +- .../shadows/ShadowKeyProviderFactory.java | 2 - .../token/web/AppSiteController.java | 4 +- gradle.properties | 2 +- .../entity/AttestationValidationStatus.java | 2 +- .../token/tools/TokenDefinition.java | 15 + 80 files changed, 2009 insertions(+), 1119 deletions(-) create mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImport.java create mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImportInterface.java create mode 100644 app/src/main/java/com/alphawallet/app/entity/attestation/SmartPassReturn.java create mode 100644 app/src/main/res/drawable/smart_pass.xml diff --git a/app/build.gradle b/app/build.gradle index 1f18dc4118..1b27ef50d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -350,7 +350,7 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' androidTestImplementation 'androidx.browser:browser:1.4.0' - implementation 'com.trustwallet:wallet-core:2.6.4' + implementation 'com.trustwallet:wallet-core:2.6.16' implementation 'com.github.florent37:TutoShowcase:d8b91be8a2' @@ -366,11 +366,10 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation platform('com.walletconnect:android-bom:1.13.1') - implementation('com.walletconnect:web3wallet:1.10.0', { + implementation("com.walletconnect:android-core", { exclude group: 'org.web3j', module: '*' }) - - implementation('com.walletconnect:android-core:1.18.0', { + implementation("com.walletconnect:web3wallet", { exclude group: 'org.web3j', module: '*' }) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6a49b470a8..daf0ffd4e9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.alphawallet.app" + android:targetSandboxVersion="1"> @@ -16,35 +16,35 @@ + android:name="android.hardware.fingerprint" + android:required="false" /> - - + android:requestLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:testOnly="false" + android:theme="@style/AppTheme.NoActionBar" + android:usesCleartextTraffic="true" + tools:replace="android:name, android:theme, android:allowBackup"> + + @@ -91,108 +91,107 @@ + android:name=".ui.ApiV1Activity" + android:theme="@style/AppTheme.NoActionBar.Transparent" + android:label="API V1 Activity" /> + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/filepaths" /> + android:name=".ui.WalletsActivity" + android:label="@string/title_account_list" /> + android:name=".ui.SplashActivity" + android:theme="@style/AppTheme.NoActionBar.Splash" /> + android:name=".service.WalletConnectService" + android:enabled="true" /> - + android:name=".service.WalletConnectV2Service" + android:enabled="true"/> + android:name=".service.AlphaWalletFirebaseMessagingService" + android:exported="false"> + android:name="com.google.firebase.messaging.default_notification_channel_id" + android:value="alphawallet" /> + android:name=".ui.WalletConnectV2Activity" + android:launchMode="singleTop" + android:label="WalletConnectV2" /> + android:name=".ui.WalletConnectNotificationActivity" + android:label="WalletConnectV2" /> + android:name=".ui.NameThisWalletActivity" + android:label="@string/name_this_wallet" + android:windowSoftInputMode="adjustResize" /> + android:name=".ui.ImportWalletActivity" + android:label="@string/title_import" + android:screenOrientation="sensor" + android:windowSoftInputMode="stateAlwaysVisible|adjustResize" /> + android:name=".ui.TransactionDetailActivity" + android:label="@string/title_transaction_details" /> + android:name=".ui.MyAddressActivity" + android:label="@string/title_my_address" /> + android:name=".ui.SendActivity" + android:label="@string/title_activity_send" + android:windowSoftInputMode="adjustResize" /> + android:name=".ui.AddTokenActivity" + android:label="@string/title_add_token" /> + android:name=".ui.AssetDisplayActivity" + android:label="@string/title_use_token" /> + android:name=".ui.RedeemSignatureDisplayActivity" + android:label="@string/title_use_token" /> + android:name=".ui.GasSettingsActivity" + android:label="@string/title_send_settings" /> + android:name=".ui.SellDetailActivity" + android:label="@string/action_sell" + android:windowSoftInputMode="adjustPan" /> + android:name=".ui.RedeemAssetSelectActivity" + android:label="@string/title_redeem" /> + android:name=".ui.ImportTokenActivity" + android:label="@string/title_import_token" /> + android:name=".ui.TransferTicketDetailActivity" + android:label="@string/label_transfer_ticket_detail" /> + android:name=".ui.TokenDetailActivity" + android:hardwareAccelerated="true" + android:label="@string/token_details" /> + android:name=".ui.Erc20DetailActivity" + android:label="@string/erc20_details" /> + android:name=".ui.TokenFunctionActivity" + android:label="@string/token_function" /> + android:name=".ui.FunctionActivity" + android:label="@string/token_function" /> + android:name=".ui.WalletActionsActivity" + android:label="Wallet Actions" /> + android:name=".ui.AddEditDappActivity" + android:label="Add DApp" /> + android:name=".ui.NetworkChooserActivity" + android:label="Select DApp Browser Network" /> + android:name=".ui.NetworkToggleActivity" + android:label="Select Network Filters" /> + android:name=".ui.TokenManagementActivity" + android:label="Manage Tokens" /> + android:name=".ui.SelectLocaleActivity" + android:label="Select Language" /> + android:name=".ui.SelectCurrencyActivity" + android:label="Select Currency" /> + android:name=".ui.ScammerWarningActivity" + android:label="Scammer Warning" + android:noHistory="true" /> + android:name=".ui.BackupKeyActivity" + android:windowSoftInputMode="stateHidden|adjustResize" /> + android:name=".ui.SupportSettingsActivity" + android:label="Support Settings" /> + android:name=".ui.AdvancedSettingsActivity" + android:label="Advanced Settings" /> + android:name=".ui.StaticViewer" + android:label="View Text" /> + android:name=".ui.TokenScriptManagementActivity" + android:label="@string/tokenscript_management" /> + android:name=".ui.TokenActivity" + android:hardwareAccelerated="true" + android:label="@string/activity_label" /> + android:name=".ui.WalletConnectActivity" + android:label="WalletConnect" + android:launchMode="singleTop" /> + android:name=".ui.BackupFlowActivity" + android:label="WalletConnect" /> + android:name=".ui.SignDetailActivity" + android:label="Sign Details" /> + android:name=".ui.WalletConnectSessionActivity" + android:label="WalletConnect Sessions" /> + android:name=".ui.TransactionSuccessActivity" + android:label="Transaction Success" /> + android:name=".ui.SetPriceAlertActivity" + android:label="Set New Alert" + android:windowSoftInputMode="adjustResize" /> + android:name=".ui.AddCustomRPCNetworkActivity" + android:label="@string/title_activity_add_custom_rpcnetwork" /> + android:name=".ui.NFTActivity" + android:label="ERC721 Master Activity" /> + android:name=".ui.Erc1155AssetSelectActivity" + android:label="ERC1155 Asset Selection" /> + android:name=".ui.NFTAssetDetailActivity" + android:hardwareAccelerated="true" + android:label="ERC1155 Asset Details" /> + android:name=".ui.Erc1155AssetListActivity" + android:hardwareAccelerated="true" + android:label="ERC1155 Asset Details" /> + android:name=".ui.TransferNFTActivity" + android:label="NFT Transfer" /> + android:name=".ui.SelectThemeActivity" + android:label="Select Mode" /> + android:name=".ui.SearchActivity" + android:label="Search Tokens" /> + android:name=".ui.QRScanning.QRScannerActivity" + android:hardwareAccelerated="true" + android:label="@string/qr_scanner" /> + android:name=".ui.SwapActivity" + android:label="Native Swap" + android:windowSoftInputMode="adjustResize" /> + android:name=".ui.SelectRouteActivity" + android:label="@string/title_select_route" /> + android:name=".ui.SelectSwapProvidersActivity" + android:label="@string/title_select_exchanges" /> + android:name=".ui.NodeStatusActivity" + android:label="@string/action_node_status" /> + android:name=".ui.CoinbasePayActivity" + android:label="@string/title_buy_with_coinbase_pay" /> + android:name=".ui.WalletDiagnosticActivity" + android:label="@string/key_diagnostic" /> + android:name=".ui.AnalyticsSettingsActivity" + android:label="@string/settings_title_analytics" /> + android:name=".ui.CrashReportSettingsActivity" + android:label="@string/settings_title_crash_reporting" /> + android:name=".ui.NotificationSettingsActivity" + android:label="@string/title_notifications" /> + android:name=".service.PriceAlertsService" + android:enabled="true" + android:exported="false" + android:stopWithTask="true" /> diff --git a/app/src/main/cpp/keys.c b/app/src/main/cpp/keys.c index e57577a508..56763b473f 100644 --- a/app/src/main/cpp/keys.c +++ b/app/src/main/cpp/keys.c @@ -270,3 +270,25 @@ Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getBlockNativeKey( JNIEnv return (*env)->NewStringUTF(env, key); #endif } + +JNIEXPORT jstring JNICALL +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getSmartPassKey( JNIEnv* env, jclass thiz ) +{ +#if (HAS_KEYS == 1) + return getDecryptedKey(env, smartpass); +#else + const jstring key = ""; + return (*env)->NewStringUTF(env, key); +#endif +} + +JNIEXPORT jstring JNICALL +Java_com_alphawallet_app_repository_KeyProviderJNIImpl_getSmartPassDevKey( JNIEnv* env, jclass thiz ) +{ +#if (HAS_KEYS == 1) + return getDecryptedKey(env, smartpassDev); +#else + const jstring key = ""; + return (*env)->NewStringUTF(env, key); +#endif +} diff --git a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java index ac609d54dc..eff26dbbfd 100644 --- a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java +++ b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java @@ -168,14 +168,12 @@ TransactionsNetworkClientType provideBlockExplorerClient( TokenRepositoryType provideTokenRepository( EthereumNetworkRepositoryType ethereumNetworkRepository, TokenLocalSource tokenLocalSource, - OkHttpClient httpClient, @ApplicationContext Context context, TickerService tickerService) { return new TokenRepository( ethereumNetworkRepository, tokenLocalSource, - httpClient, context, tickerService); } 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 bdfaf2a92d..d4b5786e62 100644 --- a/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java +++ b/app/src/main/java/com/alphawallet/app/entity/ContractInteract.java @@ -47,11 +47,16 @@ public Single getScriptFileURI() private String loadMetaData(String tokenURI) { - if (TextUtils.isEmpty(tokenURI)) return ""; + if (TextUtils.isEmpty(tokenURI)) + { + return ""; + } + else if (Utils.isJson(tokenURI)) + { + return tokenURI; + } //check if this is direct metadata, some tokens do this - if (Utils.isJson(tokenURI)) return tokenURI; - setupClient(); return client.getContent(tokenURI); diff --git a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java index 30bc1cca0b..b8ef342980 100644 --- a/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java +++ b/app/src/main/java/com/alphawallet/app/entity/EasAttestation.java @@ -1,8 +1,9 @@ package com.alphawallet.app.entity; +import android.text.TextUtils; + import com.alphawallet.app.entity.attestation.AttestationCoreData; -import com.alphawallet.app.service.AssetDefinitionService; import com.alphawallet.app.service.KeystoreAccountService; import com.alphawallet.token.tools.Numeric; @@ -25,7 +26,7 @@ public class EasAttestation public long v; public String recipient; public String uid; - public String schema; + private String schema; public String signer; public long time; public long expirationTime; @@ -33,8 +34,10 @@ public class EasAttestation public boolean revocable; public String data; public long nonce; + public long messageVersion; + private String refSchema; - public EasAttestation(String version, long chainId, String verifyingContract, String r, String s, long v, String signer, String uid, String schema, String recipient, long time, long expirationTime, String refUID, boolean revocable, String data, long nonce) + public EasAttestation(String version, long chainId, String verifyingContract, String r, String s, long v, String signer, String uid, String schema, String recipient, long time, long expirationTime, String refUID, boolean revocable, String data, long nonce, long messageVersion) { this.version = version; this.chainId = chainId; @@ -52,6 +55,8 @@ public EasAttestation(String version, long chainId, String verifyingContract, St this.revocable = revocable; this.data = data; this.nonce = nonce; + this.refSchema = null; + this.messageVersion = messageVersion; } public String getVersion() @@ -140,7 +145,21 @@ public void setUid(String uid) public String getSchema() { - if (schema.equals("0")) + BigInteger schemaVal = new BigInteger(Numeric.cleanHexPrefix(schema), 16); + if (schemaVal.equals(BigInteger.ZERO)) + { + return getRefSchema(); + } + else + { + return schema; + } + } + + private String getFixedSchema() + { + BigInteger schemaVal = new BigInteger(Numeric.cleanHexPrefix(schema), 16); + if (schemaVal.equals(BigInteger.ZERO)) { return Numeric.toHexStringWithPrefixZeroPadded(BigInteger.ZERO, 64); } @@ -150,6 +169,18 @@ public String getSchema() } } + private String getRefSchema() + { + if (TextUtils.isEmpty(refSchema)) + { + return Numeric.toHexStringWithPrefixZeroPadded(BigInteger.ZERO, 64); + } + else + { + return refSchema; + } + } + public void setSchema(String schema) { this.schema = schema; @@ -258,6 +289,10 @@ public String getEIP712Attestation() types.put("EIP712Domain", jsonType); JSONArray attest = new JSONArray(); + if (messageVersion > 0) + { + putElement(attest, "version", "uint16"); + } putElement(attest, "schema", "bytes32"); putElement(attest, "recipient", "address"); putElement(attest, "time", "uint64"); @@ -312,13 +347,17 @@ public String getEIP712Message() private JSONObject formMessage() throws Exception { JSONObject jsonMessage = new JSONObject(); + if (messageVersion > 0) + { + jsonMessage.put("version", messageVersion); + } jsonMessage.put("time", time); jsonMessage.put("data", data); jsonMessage.put("expirationTime", expirationTime); jsonMessage.put("recipient", recipient); jsonMessage.put("refUID", getRefUID()); jsonMessage.put("revocable", revocable); - jsonMessage.put("schema", getSchema()); + jsonMessage.put("schema", getFixedSchema()); return jsonMessage; } @@ -334,38 +373,14 @@ private void putElement(JSONArray jsonType, String name, String type) throws Exc public AttestationCoreData getAttestationCore() { - /* -verifyEASAttestation((bytes32,address,uint64,uint64,bool,bytes32,bytes),bytes) -struct AttestationCoreData { - bytes32 schema; // The UID of the associated EAS schema - address recipient; // The recipient of the attestation. - uint64 time; // The time when the attestation is valid from (Unix timestamp). - uint64 expirationTime; // The time when the attestation expires (Unix timestamp). - bool revocable; // Whether the attestation is revocable. - bytes32 refUID; // The UID of the related attestation. - bytes data; // The actual Schema data (eg eventId: 12345, ticketId: 6 etc) -} - */ - - /*return new AttestationCoreData(new Address(recipient), time, expirationTime, revocable, - Numeric.toBytesPadded(new BigInteger(refUID), 32), - Numeric.hexStringToByteArray(data), BigInteger.ZERO, - Numeric.hexStringToByteArray(schema));*/ - - BigInteger bi = new BigInteger(refUID); - - byte[] lala = Numeric.toBytesPadded(bi, 32); - - BigInteger bi2 = Numeric.toBigInt(schema); - - byte[] lala2 = Numeric.toBytesPadded(bi2, 32); - - Address l = new Address(recipient); - - byte[] bib = Numeric.hexStringToByteArray(data); + BigInteger refVal = new BigInteger(refUID); + byte[] refBytes = Numeric.toBytesPadded(refVal, 32); + BigInteger schemaVal = Numeric.toBigInt(schema); + byte[] schemaBytes = Numeric.toBytesPadded(schemaVal, 32); + byte[] dataBytes = Numeric.hexStringToByteArray(data); - return new AttestationCoreData(lala2, - new Address(recipient), time, expirationTime, revocable, lala, - bib); + return new AttestationCoreData(schemaBytes, + new Address(recipient), time, expirationTime, revocable, refBytes, + dataBytes); } } diff --git a/app/src/main/java/com/alphawallet/app/entity/SuggestEIP1559.kt b/app/src/main/java/com/alphawallet/app/entity/SuggestEIP1559.kt index 07a710a9ff..c702489548 100644 --- a/app/src/main/java/com/alphawallet/app/entity/SuggestEIP1559.kt +++ b/app/src/main/java/com/alphawallet/app/entity/SuggestEIP1559.kt @@ -140,7 +140,8 @@ internal fun suggestPriorityFee(firstBlock: Long, feeHistory: FeeHistory, gasSer val blockCount = maxBlockCount(gasUsedRatio, ptr, needBlocks) if (blockCount > 0) { // feeHistory API call with reward percentile specified is expensive and therefore is only requested for a few non-full recent blocks. - val feeHistoryFetch = gasService.getChainFeeHistory(blockCount, "0x" + (firstBlock + ptr).toString(16), + val feeHistoryFetch = gasService.getChainFeeHistory(blockCount, + Numeric.prependHexPrefix((firstBlock + ptr).toString(16)), rewardPercentile.toString()).blockingGet(); val rewardSize = feeHistoryFetch?.reward?.size ?: 0 diff --git a/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java b/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java index 2a22edb640..7ba53f11de 100644 --- a/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java +++ b/app/src/main/java/com/alphawallet/app/entity/TransactionDecoder.java @@ -157,7 +157,7 @@ private ParseStage getParams(TransactionInput thisData, String input) BigInteger dataCount = Numeric.toBigInt(argData); String hexBytes = readBytes(input, dataCount.intValue()); thisData.miscData.add(hexBytes); - thisData.hexArgs.add("0x" + hexBytes); + thisData.hexArgs.add(Numeric.prependHexPrefix(hexBytes)); break; case "string": count = new BigInteger(argData, 16); @@ -179,7 +179,7 @@ private ParseStage getParams(TransactionInput thisData, String input) case "address": if (argData.length() >= 64 - ADDRESS_LENGTH_IN_HEX) { - String addr = "0x" + argData.substring(64 - ADDRESS_LENGTH_IN_HEX); + String addr = Numeric.prependHexPrefix(argData.substring(64 - ADDRESS_LENGTH_IN_HEX)); thisData.addresses.add(addr); thisData.hexArgs.add(addr); } diff --git a/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java b/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java index 03ecb925e3..fdb5f4c5e0 100644 --- a/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java +++ b/app/src/main/java/com/alphawallet/app/entity/TransactionInput.java @@ -324,7 +324,7 @@ private String getMagicLinkAddress(Transaction tx) byte[] tradeBytes = parser.getTradeBytes(ticketIndexArray, contractAddress, priceWei, expiry); //attempt ecrecover BigInteger key = Sign.signedMessageToKey(tradeBytes, sig); - address = "0x" + Keys.getAddress(key); + address = Numeric.prependHexPrefix(Keys.getAddress(key)); } catch (Exception e) { diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImport.java b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImport.java new file mode 100644 index 0000000000..f060c01a12 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImport.java @@ -0,0 +1,768 @@ +package com.alphawallet.app.entity.attestation; + +import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; +import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; +import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_ID; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.alphawallet.app.entity.ContractType; +import com.alphawallet.app.entity.EasAttestation; +import com.alphawallet.app.entity.NetworkInfo; +import com.alphawallet.app.entity.QRResult; +import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.tokendata.TokenGroup; +import com.alphawallet.app.entity.tokens.Attestation; +import com.alphawallet.app.entity.tokens.Token; +import com.alphawallet.app.entity.tokens.TokenCardMeta; +import com.alphawallet.app.entity.tokens.TokenInfo; +import com.alphawallet.app.repository.EthereumNetworkBase; +import com.alphawallet.app.repository.KeyProvider; +import com.alphawallet.app.repository.KeyProviderFactory; +import com.alphawallet.app.repository.entity.RealmAttestation; +import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.service.RealmManager; +import com.alphawallet.app.service.TokensService; +import com.alphawallet.app.util.Utils; +import com.alphawallet.app.web3j.StructuredDataEncoder; +import com.alphawallet.token.entity.AttestationDefinition; +import com.alphawallet.token.entity.AttestationValidationStatus; +import com.alphawallet.token.entity.FunctionDefinition; +import com.alphawallet.token.tools.Numeric; +import com.alphawallet.token.tools.TokenDefinition; +import com.bumptech.glide.RequestBuilder; +import com.google.gson.Gson; + +import org.json.JSONException; +import org.json.JSONObject; +import org.web3j.abi.FunctionReturnDecoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.Address; +import org.web3j.abi.datatypes.Bool; +import org.web3j.abi.datatypes.DynamicBytes; +import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.Type; +import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.generated.Bytes32; +import org.web3j.abi.datatypes.generated.Uint256; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import io.realm.Realm; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import timber.log.Timber; + +public class AttestationImport +{ + private final AssetDefinitionService assetDefinitionService; + private final AttestationImportInterface callback; + private final TokensService tokensService; + private final Wallet wallet; + private final RealmManager realmManager; + private final OkHttpClient client; + private final KeyProvider keyProvider = KeyProviderFactory.get(); + + public static final String SMART_LAYER_DOMAIN = "https://www.smartlayer.network/pass"; + public static final String SMART_LAYER_DOMAIN_DEV = "https://smart-layer.vercel.app/pass"; + public static final String SMART_PASS_URL = "https://aw.app/openurl?url="; + + private static final String SMART_PASS_API = "https://backend.smartlayer.network/passes/pass-installed-in-aw"; + private static final String SMART_PASS_API_DEV = "https://d2a5tt41o5qmyt.cloudfront.net/passes/pass-installed-in-aw"; + + public AttestationImport(AssetDefinitionService assetService, + TokensService tokensService, + AttestationImportInterface assetInterface, + Wallet wallet, + RealmManager realm, + OkHttpClient client) + { + this.assetDefinitionService = assetService; + this.tokensService = tokensService; + this.callback = assetInterface; + this.wallet = wallet; + this.realmManager = realm; + this.client = client; + } + + public void importAttestation(QRResult attestation) + { + switch (attestation.type) + { + case ATTESTATION: + importLegacyAttestation(attestation); + break; + case EAS_ATTESTATION: + importEASAttestation(attestation); + break; + default: + break; + } + } + + private void importLegacyAttestation(QRResult attestation) + { + //Get token information - assume attestation is based on NFT + //TODO: First validate Attestation + tokensService.update(attestation.getAddress(), attestation.chainId, ContractType.ERC721) + .flatMap(tInfo -> tokensService.storeTokenInfoDirect(wallet, tInfo, ContractType.ERC721)) + .flatMap(tInfo -> storeAttestation(attestation, tInfo)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(attn -> completeImport(attestation, attn), err -> callback.importError(err.getMessage())) + .isDisposed(); + } + + private void completeImport(QRResult attestation, Attestation tokenAttn) + { + if (tokenAttn.isValid() == AttestationValidationStatus.Pass) + { + TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), + assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); + tcmAttestation.isEnabled = true; + callback.attestationImported(tcmAttestation); + } + else + { + callback.importError(tokenAttn.isValid().getValue()); + } + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + private Single storeAttestation(QRResult attestation, TokenInfo tInfo) + { + Attestation attn = validateAttestation(attestation.getAttestation(), tInfo); + switch (attn.isValid()) + { + case Pass: + return storeAttestationInternal(attestation, tInfo, attn); + case Expired: + case Issuer_Not_Valid: + case Incorrect_Subject: + callback.importError(attn.isValid().getValue()); + break; + } + + return Single.fromCallable(() -> attn); + } + + private Single storeAttestationInternal(QRResult attestation, TokenInfo tInfo, Attestation attn) + { + //complete the import + //write to realm + return Single.fromCallable(() -> { + try (Realm realm = realmManager.getRealmInstance(wallet)) + { + realm.executeTransaction(r -> { + +// RealmResults realmAssets = realm.where(RealmAttestation.class) +// .findAll(); +// +// realmAssets.deleteAllFromRealm(); + + String key = attn.getDatabaseKey(); + RealmAttestation realmAttn = r.where(RealmAttestation.class) + .equalTo("address", key) + .findFirst(); + + if (realmAttn == null) + { + realmAttn = r.createObject(RealmAttestation.class, key); + } + + attn.populateRealmAttestation(realmAttn); + realmAttn.setAttestation(attn.getAttestation()); + }); + } + catch (Exception e) + { + e.printStackTrace(); + } + return attn; + }).flatMap(generatedAttestation -> { + if (tokensService.getToken(tInfo.chainId, tInfo.address) == null) + { + return tokensService.storeTokenInfo(wallet, tInfo, ContractType.ERC721); + } + else + { + return Single.fromCallable(() -> tInfo); + } + }).map(info -> setBaseType(attn, info)); + } + + private Attestation setBaseType(Attestation attn, TokenInfo info) + { + Token baseToken = tokensService.getToken(info.chainId, info.address); + if (baseToken != null) + { + attn.setBaseTokenType(baseToken.getInterfaceSpec()); + } + + return attn; + } + + private void importEASAttestation(QRResult qrAttn) + { + //validate attestation + //get chain and address + EasAttestation easAttn = new Gson().fromJson(qrAttn.functionDetail, EasAttestation.class); + + //validation UID: + storeAttestation(easAttn, qrAttn.functionDetail, qrAttn.getAddress()) + .flatMap(attn -> callSmartPassLog(attn, qrAttn.getAddress())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::checkTokenScript, err -> callback.importError(err.getMessage())) + .isDisposed(); + } + + @SuppressWarnings("checkstyle:MissingSwitchDefault") + private Single storeAttestation(EasAttestation attestation, String importedAttestation, String originLink) + { + //Use Default key unless specified + return Single.fromCallable(() -> { + Attestation attn = loadAttestation(attestation, originLink); + switch (attn.isValid()) + { + case Pass: + return storeAttestationInternal(originLink, attn); + case Expired: + case Issuer_Not_Valid: + case Incorrect_Subject: + callback.importError(attn.isValid().getValue()); + break; + } + + return attn; + }); + } + + private Attestation storeAttestationInternal(String originLink, Attestation attn) + { + try (Realm realm = realmManager.getRealmInstance(wallet)) + { + realm.executeTransaction(r -> { + String key = attn.getDatabaseKey(); + RealmAttestation realmAttn = r.where(RealmAttestation.class) + .equalTo("address", key) + .findFirst(); + + if (realmAttn == null) + { + realmAttn = r.createObject(RealmAttestation.class, key); + } + + realmAttn.setAttestationLink(originLink); + attn.populateRealmAttestation(realmAttn); + }); + } + catch (Exception e) + { + e.printStackTrace(); + } + return attn; + } + + private void completeImport(Token token) + { + if (token instanceof Attestation && ((Attestation)token).isValid() == AttestationValidationStatus.Pass) + { + Attestation tokenAttn = (Attestation)token; + TokenCardMeta tcmAttestation = new TokenCardMeta(tokenAttn.tokenInfo.chainId, tokenAttn.getAddress(), "1", System.currentTimeMillis(), + assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), + TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); + tcmAttestation.isEnabled = true; + callback.attestationImported(tcmAttestation); + } + } + + private void checkTokenScript(Token token) + { + //check server for a TokenScript + assetDefinitionService.checkServerForScript(token, null) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(td -> completeImport(token), Timber::w) + .isDisposed(); + } + + //Handling + public Attestation validateAttestation(String attestation, TokenInfo tInfo) + { + TokenDefinition td = assetDefinitionService.getDefinition(getTSDataKeyTemp(tInfo.chainId, tInfo.address));//getDefinition(tInfo.chainId, tInfo.address); + Attestation att = null; + + if (td != null) + { + NetworkInfo networkInfo = EthereumNetworkBase.getNetwork(tInfo.chainId); + att = new Attestation(tInfo, networkInfo.name, Numeric.hexStringToByteArray(attestation)); + att.setTokenWallet(tokensService.getCurrentAddress()); + + //call validation function and get details + AttestationDefinition definitionAtt = td.getAttestation(); + //can we get the details? + + if (definitionAtt != null && definitionAtt.function != null) + { + //pull return type + FunctionDefinition fd = definitionAtt.function; + //add attestation to attr map + //call function + org.web3j.abi.datatypes.Function transaction = assetDefinitionService.generateTransactionFunction(att, BigInteger.ZERO, td, fd); + transaction = new Function(fd.method, transaction.getInputParameters(), td.getAttestationReturnTypes()); //set return types + + //call and handle result + String result = assetDefinitionService.callSmartContract(tInfo.chainId, tInfo.address, transaction); + + //break down result + List values = FunctionReturnDecoder.decode(result, transaction.getOutputParameters()); + + //interpret these values + att.handleValidation(td.getValidation(values)); + } + } + + return att; + } + + public Attestation loadAttestation(EasAttestation attestation, String originLink) + { + String recoverAttestationSigner = recoverSigner(attestation); + + //1. Validate signer via key attestation service (using UID). + boolean issuerOnKeyChain = checkAttestationSigner(attestation, recoverAttestationSigner); + + //2. Decode the ABI encoded payload to pull out the info. ABI Decode the schema bytes + //initially we need a hardcoded schema - this should be fetched from the schema record EAS contract + //fetch the schema of the attestation + SchemaRecord attestationSchema = fetchSchemaRecord(attestation.getChainId(), attestation.getSchema()); + //convert into functionDecode + List names = new ArrayList<>(); + List values = decodeAttestationData(attestation.data, attestationSchema.schema, names); + + NetworkInfo networkInfo = EthereumNetworkBase.getNetwork(attestation.getChainId()); + + TokenInfo tInfo = Attestation.getDefaultAttestationInfo(attestation.getChainId(), getEASContract(attestation.chainId)); + Attestation localAttestation = new Attestation(tInfo, networkInfo.name, originLink.getBytes(StandardCharsets.UTF_8)); + localAttestation.handleEASAttestation(attestation, names, values, recoverAttestationSigner); + + String collectionHash = localAttestation.getAttestationCollectionId(); + + //is it a smartpass? + if (localAttestation.hasSmartPassElement() && issuerOnKeyChain) + { + tInfo = Attestation.getSmartPassInfo(attestation.getChainId(), collectionHash); + } + else + { + tInfo = Attestation.getDefaultAttestationInfo(attestation.getChainId(), collectionHash); + } + + //Now regenerate with the correct collectionId + localAttestation = new Attestation(tInfo, networkInfo.name, originLink.getBytes(StandardCharsets.UTF_8)); + localAttestation.handleEASAttestation(attestation, names, values, recoverAttestationSigner); + localAttestation.setTokenWallet(tokensService.getCurrentAddress()); + + return localAttestation; + } + + private boolean checkAttestationSigner(EasAttestation attestation, String recoverAttestationSigner) + { + String keySchemaUID = getKeySchemaUID(attestation.getChainId()); + boolean attestationValid; + if (Attestation.getKnownRootIssuers(attestation.chainId).contains(recoverAttestationSigner)) + { + attestationValid = true; + } + else if (!TextUtils.isEmpty(keySchemaUID)) + { + //call validate + SchemaRecord schemaRecord = fetchSchemaRecord(attestation.getChainId(), keySchemaUID); + attestationValid = checkAttestationIssuer(schemaRecord, attestation.getChainId(), recoverAttestationSigner); + } + else + { + attestationValid = false; + } + + return attestationValid; + } + + private String getDataValue(String key, List names, List values) + { + Map valueMap = new HashMap<>(); + for (int index = 0; index < names.size(); index++) + { + String name = names.get(index); + Type type = values.get(index); + valueMap.put(name, type.toString()); + } + + return valueMap.get(key); + } + + private List decodeAttestationData(String attestationData, @NonNull String decodeSchema, List names) + { + List> returnTypes = new ArrayList>(); + if (TextUtils.isEmpty(decodeSchema)) + { + return new ArrayList<>(); + } + + //build decoder + String[] typeData = decodeSchema.split(","); + for (String typeElement : typeData) + { + String[] data = typeElement.split(" "); + String type = data[0]; + String name = data[1]; + if (type.startsWith("uint") || type.startsWith("int")) + { + type = "uint"; + } + else if (type.startsWith("bytes") && !type.equals("bytes")) + { + type = "bytes32"; + } + + TypeReference tRef = null; + + switch (type) + { + case "uint": + tRef = new TypeReference() { }; + break; + case "address": + tRef = new TypeReference
() { }; + break; + case "bytes32": + tRef = new TypeReference() { }; + break; + case "string": + tRef = new TypeReference() { }; + break; + case "bytes": + tRef = new TypeReference() { }; + break; + case "bool": + tRef = new TypeReference() { }; + break; + default: + break; + } + + if (tRef != null) + { + returnTypes.add(tRef); + } + else + { + Timber.e("Unhandled type!"); + returnTypes.add(new TypeReference() { }); + } + + names.add(name); + } + + //decode the schema and populate the Attestation element + return FunctionReturnDecoder.decode(attestationData, org.web3j.abi.Utils.convert(returnTypes)); + } + + private SchemaRecord fetchSchemaRecord(long chainId, String schemaUID) + { + SchemaRecord schemaRecord = tryCachedValues(schemaUID); + + if (schemaRecord == null) + { + schemaRecord = fetchSchemaRecordOnChain(chainId, schemaUID); + } + + return schemaRecord; + } + + private SchemaRecord fetchSchemaRecordOnChain(long chainId, String schemaUID) + { + //1. Resolve UID. For now, just use default: This should be on a switch for chains + String globalResolver = getEASSchemaContract(chainId); + + //format transaction to get key resolver + Function getKeyResolver2 = new Function("getSchema", + Collections.singletonList(new Bytes32(Numeric.hexStringToByteArray(schemaUID))), + Collections.singletonList(new TypeReference() {})); + + String result = assetDefinitionService.callSmartContract(chainId, globalResolver, getKeyResolver2); + List values = FunctionReturnDecoder.decode(result, getKeyResolver2.getOutputParameters()); + + return (SchemaRecord)values.get(0); + } + + private SchemaRecord tryCachedValues(String schemaUID) //doesn't matter about chain clash - if schemaUID matches then the schema is the same + { + return getCachedSchemaRecords().getOrDefault(schemaUID, null); + } + + private boolean checkAttestationIssuer(SchemaRecord schemaRecord, long chainId, String signer) + { + String rootKeyUID = getDefaultRootKeyUID(chainId); + //pull the key resolver + Address resolverAddr = schemaRecord.resolver; + //call the resolver to test key validity + Function validateKey = new Function("validateSignature", + Arrays.asList((new Bytes32(Numeric.hexStringToByteArray(rootKeyUID))), + new Address(signer)), + Collections.singletonList(new TypeReference() {})); + + String result = assetDefinitionService.callSmartContract(chainId, resolverAddr.getValue(), validateKey); + List values = FunctionReturnDecoder.decode(result, validateKey.getOutputParameters()); + return ((Bool)values.get(0)).getValue(); + } + + public static String recoverSigner(EasAttestation attestation) + { + String recoveredAddress = ""; + + try + { + StructuredDataEncoder dataEncoder = new StructuredDataEncoder(attestation.getEIP712Attestation()); + byte[] hash = dataEncoder.hashStructuredData(); + byte[] r = Numeric.hexStringToByteArray(attestation.getR()); + byte[] s = Numeric.hexStringToByteArray(attestation.getS()); + byte v = (byte)(attestation.getV() & 0xFF); + + Sign.SignatureData sig = new Sign.SignatureData(v, r, s); + BigInteger key = Sign.signedMessageHashToKey(hash, sig); + recoveredAddress = Numeric.prependHexPrefix(Keys.getAddress(key)); + } + catch (Exception e) + { + e.printStackTrace(); + } + + return recoveredAddress; + } + + // NB Java 11 doesn't have support for switching on a 'long' :( + public static String getEASContract(long chainId) + { + if (chainId == MAINNET_ID) + { + return "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587"; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"; + } + else + { + //Support Optimism Goerli (0xC2679fBD37d54388Ce493F1DB75320D236e1815e) + return ""; + } + } + + private String getEASSchemaContract(long chainId) + { + if (chainId == MAINNET_ID) + { + return "0xA7b39296258348C78294F95B872b282326A97BDF"; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0xA310da9c5B885E7fb3fbA9D66E9Ba6Df512b78eB"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } + + //UID of schema used for keys on each chain - the resolver is tied to this UID + private String getKeySchemaUID(long chainId) + { + if (chainId == MAINNET_ID) + { + return ""; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } + + // If not specified + private String getDefaultRootKeyUID(long chainId) + { + if (chainId == MAINNET_ID) + { + return ""; + } + else if (chainId == ARBITRUM_MAIN_ID) + { + return "0xe5c2bfd98a1b35573610b4e5a367bbcb5c736e42508a33fd6046bad63eaf18f9"; + } + else if (chainId == SEPOLIA_TESTNET_ID) + { + return "0xee99de42f544fa9a47caaf8d4a4426c1104b6d7a9df7f661f892730f1b5b1e23"; + } + else + { + //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) + return ""; + } + } + + private Map getCachedSchemaRecords() + { + Map recordMap = new HashMap<>(); + + SchemaRecord keySchema = new SchemaRecord(Numeric.hexStringToByteArray("0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1") + , new Address("0x0ed88b8af0347ff49d7e09aa56bd5281165225b6"), true, "string KeyDescription,bytes ASN1Key,bytes PublicKey"); + SchemaRecord keySchema2 = new SchemaRecord(Numeric.hexStringToByteArray("0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4") + , new Address("0xF0768c269b015C0A246157c683f9377eF571dCD3"), true, "string KeyDescription,bytes ASN1Key,bytes PublicKey"); + SchemaRecord smartPass = new SchemaRecord(Numeric.hexStringToByteArray("0x7f6fb09beb1886d0b223e9f15242961198dd360021b2c9f75ac879c0f786cafd") + , new Address(ZERO_ADDRESS), true, "string eventId,string ticketId,uint8 ticketClass,bytes commitment"); + SchemaRecord smartPass2 = new SchemaRecord(Numeric.hexStringToByteArray("0x0630f3342772bf31b669bdbc05af0e9e986cf16458f292dfd3b57564b3dc3247") + , new Address(ZERO_ADDRESS), true, "string devconId,string ticketIdString,uint8 ticketClass,bytes commitment"); + SchemaRecord smartPassMainNetLegacy = new SchemaRecord(Numeric.hexStringToByteArray("0xba8aaaf91d1f63d998fb7da69449d9a314bef480e9555710c77d6e594e73ca7a") + , new Address(ZERO_ADDRESS), true, "string eventId,string ticketId,uint8 ticketClass,bytes commitment,string scriptUri"); + SchemaRecord smartPassMainNet = new SchemaRecord(Numeric.hexStringToByteArray("0x44ec5251add2115c92896cf4b531eb2fcfac6d8ec8caa451df52f0a25a028545") + , new Address(ZERO_ADDRESS), true, "uint16 version,string orgId,string memberId,string memberRole,bytes commitment,string scriptURI"); + + recordMap.put(Numeric.toHexString(keySchema.uid), keySchema); + recordMap.put(Numeric.toHexString(keySchema2.uid), keySchema2); + recordMap.put(Numeric.toHexString(smartPass.uid), smartPass); + recordMap.put(Numeric.toHexString(smartPass2.uid), smartPass2); + recordMap.put(Numeric.toHexString(smartPassMainNetLegacy.uid), smartPassMainNetLegacy); + recordMap.put(Numeric.toHexString(smartPassMainNet.uid), smartPassMainNet); + + return recordMap; + } + + private String getTSDataKeyTemp(long chainId, String address) + { + if (address.equalsIgnoreCase(tokensService.getCurrentAddress())) + { + address = "ethereum"; + } + + return address.toLowerCase(Locale.ROOT) + "-" + chainId; + } + + + //Smart Pass handling + private Single callSmartPassLog(Attestation attn, String fullPass) + { + return Single.fromCallable(() -> { + //check if attestation is valid, and if it's a smartpass + if (attn.isValid() == AttestationValidationStatus.Pass && attn.isSmartPass()) + { + callback.smartPassValidation(callPassConfirmAPI(attn, fullPass)); + } + + return attn; + }); + } + + //call API if required + private SmartPassReturn callPassConfirmAPI(Attestation attn, String magicLink) + { + //need to send the raw attestation (not processed) + //isolate the pass + String rawPass = Utils.extractRawAttestation(magicLink); + if (TextUtils.isEmpty(rawPass)) + { + return SmartPassReturn.IMPORT_FAILED; //Should not happen if we get to this stage! + } + + Request.Builder builder = new Request.Builder(); + + if (EthereumNetworkBase.hasRealValue(attn.tokenInfo.chainId)) + { + builder.url(SMART_PASS_API) + .header("Authorization", "Bearer " + keyProvider.getSmartPassKey()); + } + else + { + builder.url(SMART_PASS_API_DEV) + .header("Authorization", "Bearer " + keyProvider.getSmartPassDevKey()); + } + + Request request = builder + .put(buildPassBody(rawPass)) + .build(); + + try (okhttp3.Response response = client.newCall(request).execute()) + { + switch (response.code()/100) + { + case 2: + return SmartPassReturn.IMPORT_SUCCESS; + case 4: + return SmartPassReturn.ALREADY_IMPORTED; + default: + case 5: + return SmartPassReturn.IMPORT_FAILED; + } + } + catch (Exception e) + { + return SmartPassReturn.NO_CONNECTION; + } + } + + private RequestBody buildPassBody(String rawPass) + { + RequestBody body = null; + try + { + JSONObject json = new JSONObject(); + json.put("signedToken", rawPass); + json.put("installedPassedInAw", 1); + body = RequestBody.create(json.toString(), MediaType.parse("application/json")); + } + catch (JSONException e) + { + Timber.w(e); + } + + return body; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImportInterface.java b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImportInterface.java new file mode 100644 index 0000000000..e215ca0812 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/attestation/AttestationImportInterface.java @@ -0,0 +1,10 @@ +package com.alphawallet.app.entity.attestation; + +import com.alphawallet.app.entity.tokens.TokenCardMeta; + +public interface AttestationImportInterface +{ + void attestationImported(TokenCardMeta newToken); + void importError(String error); + void smartPassValidation(SmartPassReturn validation); +} diff --git a/app/src/main/java/com/alphawallet/app/entity/attestation/SmartPassReturn.java b/app/src/main/java/com/alphawallet/app/entity/attestation/SmartPassReturn.java new file mode 100644 index 0000000000..1eefc0f455 --- /dev/null +++ b/app/src/main/java/com/alphawallet/app/entity/attestation/SmartPassReturn.java @@ -0,0 +1,21 @@ +package com.alphawallet.app.entity.attestation; + +public enum SmartPassReturn +{ + ALREADY_IMPORTED("Already Imported"), + IMPORT_SUCCESS("Import Success"), + IMPORT_FAILED("Pass not valid"), + NO_CONNECTION("Could not connect to SmartLayer, try again later"); + + private final String property; + + SmartPassReturn(String property) + { + this.property = property; + } + + public String getValue() + { + return property; + } +} diff --git a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java index b71e029a8d..521743226f 100644 --- a/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java +++ b/app/src/main/java/com/alphawallet/app/entity/nftassets/NFTAsset.java @@ -3,7 +3,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.util.Pair; import androidx.annotation.Nullable; 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 4762d56086..5eb412aaf0 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 @@ -1,5 +1,6 @@ package com.alphawallet.app.entity.tokens; +import static com.alphawallet.app.entity.tokenscript.TokenscriptFunction.ZERO_ADDRESS; import static com.alphawallet.app.repository.TokensRealmSource.attestationDatabaseKey; import android.content.Context; @@ -9,8 +10,9 @@ import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.nftassets.NFTAsset; +import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.entity.RealmAttestation; -import com.alphawallet.app.service.AssetDefinitionService; +import com.alphawallet.app.util.Utils; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.AttestationValidation; @@ -21,6 +23,7 @@ import com.google.gson.Gson; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Type; @@ -31,7 +34,7 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -49,8 +52,6 @@ public class Attestation extends Token private final byte[] attestation; private String attestationSubject; private String issuerKey; - private boolean issuerValid; - private String issuerAddress; private long validFrom; private long validUntil; private final Map additionalMembers = new HashMap<>(); @@ -60,13 +61,15 @@ public class Attestation extends Token private static final String VALID_TO = "expirationTime"; private static final String TICKET_ID = "TicketId"; private static final String SCRIPT_URI = "scriptURI"; - private static final String EVENT_ID = "eventId"; + private static final String EVENT_ID = "orgId"; private static final String SCHEMA_DATA_PREFIX = "data."; public static final String ATTESTATION_SUFFIX = "-att"; - + public static final String EAS_ATTESTATION_TEXT = "EAS Attestation"; + public static final String EAS_ATTESTATION_SYMBOL = "ATTN"; + public static final String SMART_PASS = "Smartpass"; + private static final String SMARTLAYER = "SL";//"SMARTLAYER"; //TODO: Supplemental data - public Attestation(TokenInfo tokenInfo, String networkName, byte[] attestation) { super(tokenInfo, BigDecimal.ONE, System.currentTimeMillis(), networkName, ContractType.ATTESTATION); @@ -74,6 +77,16 @@ public Attestation(TokenInfo tokenInfo, String networkName, byte[] attestation) setAttributeResult(BigInteger.ONE, new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ONE, Numeric.toHexString(attestation))); } + public static TokenInfo getDefaultAttestationInfo(long chainId, String collectionHash) + { + return new TokenInfo(collectionHash, EAS_ATTESTATION_TEXT, EAS_ATTESTATION_SYMBOL, 0, true, chainId); + } + + public static TokenInfo getSmartPassInfo(long chainId, String collectionHash) + { + return new TokenInfo(collectionHash, SMART_PASS, EAS_ATTESTATION_SYMBOL, 0, true, chainId); + } + public byte[] getAttestation() { return attestation; @@ -87,10 +100,8 @@ public void handleValidation(AttestationValidation attValidation) } attestationSubject = attValidation._subjectAddress; - issuerAddress = attValidation._issuerAddress; isValid = attValidation._isValid; issuerKey = attValidation._issuerKey; - issuerValid = attValidation._issuerValid || (!TextUtils.isEmpty(issuerKey) && (TextUtils.isEmpty(issuerAddress) || !issuerKey.equalsIgnoreCase(issuerAddress))); for (Map.Entry> t : attValidation.additionalMembers.entrySet()) { @@ -101,7 +112,7 @@ public void handleValidation(AttestationValidation attValidation) additionalMembers.put(SCHEMA_DATA_PREFIX + TICKET_ID, ticketId); } - public void handleEASAttestation(EasAttestation attn, List names, List values, boolean isAttestationValid) + public void handleEASAttestation(EasAttestation attn, List names, List values, String issuer) { //add members for (int index = 0; index < names.size(); index++) @@ -111,8 +122,7 @@ public void handleEASAttestation(EasAttestation attn, List names, List type) additionalMembers.put(name, new MemberData(name, type)); } - private void populateMembersFromJSON(String jsonData) + private static Map getMembersFromJSON(String jsonData) { + Map members = new HashMap<>(); try { JSONArray elements = new JSONArray(jsonData); @@ -414,13 +430,15 @@ private void populateMembersFromJSON(String jsonData) for (index = 0; index < elements.length(); index++) { JSONObject element = elements.getJSONObject(index); - additionalMembers.put(element.getString("name"), new MemberData(element)); + members.put(element.getString("name"), new MemberData(element)); } } catch (Exception e) { Timber.e(e); } + + return members; } public EasAttestation getEasAttestation() @@ -428,8 +446,8 @@ public EasAttestation getEasAttestation() try { String rawAttestation = new String(attestation, StandardCharsets.UTF_8); - EasAttestation easAttn = new Gson().fromJson(rawAttestation, EasAttestation.class); - return easAttn; + String taglessAttestation = Utils.parseEASAttestation(rawAttestation); + return new Gson().fromJson(Utils.toAttestationJson(taglessAttestation), EasAttestation.class); } catch (Exception e) //Expected { @@ -452,6 +470,30 @@ public String getAttestationName(TokenDefinition td) } } + public static boolean hasSmartPassElementCheck(String jsonData) + { + return hasSmartPassElementInternal(getMembersFromJSON(jsonData)); + } + + public boolean hasSmartPassElement() + { + return hasSmartPassElementInternal(additionalMembers); + } + + private static boolean hasSmartPassElementInternal(Map members) + { + MemberData orgId = members.getOrDefault("data.orgId", null); + String orgIdValue = orgId != null ? orgId.getString() : ""; + return orgIdValue.equalsIgnoreCase(SMARTLAYER); + } + + public boolean isSmartPass() + { + //find SMARTPASS in the schema elements and check against correct issuer. TODO: check against keychain if not valid + boolean issuerMatch = getKnownRootIssuers(tokenInfo.chainId).contains(issuerKey); + return hasSmartPassElementInternal(additionalMembers) && issuerMatch; + } + private static class MemberData { JSONObject element; @@ -534,6 +576,23 @@ public String getEncoding() return element.toString(); } + public String getCleanKey() + { + try + { + String name = element.get("name").toString(); + if (name.startsWith(SCHEMA_DATA_PREFIX)) + { + name = name.substring(SCHEMA_DATA_PREFIX.length()); + } + return name; + } + catch (JSONException e) + { + return ""; + } + } + public BigInteger getValue() { try @@ -732,4 +791,20 @@ public BigInteger getUUID() return BigInteger.ONE; } } + + public static List getKnownRootIssuers(long chainId) + { + List knownIssuers = new ArrayList<>(); + // Add production keys - these should work on any issued attestation + knownIssuers.add("0x715e50699db0a553119a4eb1cd13808eedc2910d"); //production key + knownIssuers.add("0xA20efc4B9537d27acfD052003e311f762620642D".toLowerCase(Locale.ROOT)); //Testkey for resolve + + // Testnet keys should only work on testnet chains + if (!EthereumNetworkBase.hasRealValue(chainId)) + { + knownIssuers.add("0x4461110869a5d65df76b85e2cd8bbfdda2ca6e4d"); + } + + return knownIssuers; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java index 771082fb20..9a3f592012 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/ERC1155Token.java @@ -649,7 +649,7 @@ public EthFilter getReceiveBalanceFilter(Event event, DefaultBlockParameter star filter.addSingleTopic(null); filter.addSingleTopic(null); - filter.addSingleTopic("0x" + TypeEncoder.encode(new Address(getWallet()))); //listen for events 'to' the wallet + filter.addSingleTopic(Numeric.prependHexPrefix(TypeEncoder.encode(new Address(getWallet())))); //listen for events 'to' the wallet return filter; } @@ -664,7 +664,7 @@ public EthFilter getSendBalanceFilter(Event event, DefaultBlockParameter startBl .addSingleTopic(EventEncoder.encode(event)); // send event format filter.addSingleTopic(null); - filter.addSingleTopic("0x" + TypeEncoder.encode(new Address(getWallet()))); //listen for events 'from' the wallet + filter.addSingleTopic(Numeric.prependHexPrefix(TypeEncoder.encode(new Address(getWallet())))); //listen for events 'from' the wallet filter.addSingleTopic(null); return filter; } 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 5926bd80df..80426b24fc 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 @@ -213,8 +213,8 @@ public String getFullName(AssetDefinitionService assetDefinition, int count) } } - public String getTokenSymbol(Token token){ - + public String getTokenSymbol(Token token) + { if (!TextUtils.isEmpty(token.tokenInfo.symbol) && token.tokenInfo.symbol.length() > 1) { return Utils.getIconisedText(token.tokenInfo.symbol); @@ -277,15 +277,7 @@ private String sanitiseString(String str) } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - { - return Html.fromHtml(str, FROM_HTML_MODE_COMPACT).toString(); - } - else - { - //noinspection deprecation - return Html.fromHtml(str).toString(); - } + return Html.fromHtml(str, FROM_HTML_MODE_COMPACT).toString(); } } @@ -307,6 +299,28 @@ else if (nameLength > 0 && (symbolLength > nameLength || symbolLength == 0)) } } + protected BigDecimal getDecimalValue(String value) + { + BigDecimal bd = BigDecimal.ZERO; + try + { + bd = new BigDecimal(value); + } + catch (Exception e) + { + try + { + bd = new BigDecimal(new BigInteger(Numeric.cleanHexPrefix(value), 16)); //try hex + } + catch (Exception hex) + { + // + } + } + + return bd; + } + public String getSymbol() { return getShortestNameOrSymbol(); @@ -571,7 +585,7 @@ public List stringHexToBigIntegerList(String integerString) public String convertValue(String prefix, EventResult vResult, int precision) { - BigDecimal val = (vResult != null) ? new BigDecimal(vResult.value) : BigDecimal.ZERO; + BigDecimal val = getDecimalValue(vResult.value); return prefix + BalanceUtils.getScaledValueFixed(val, tokenInfo.decimals, precision); } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java index c5b8843e8f..9c08f5bc1d 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokens/TokenFactory.java @@ -3,11 +3,17 @@ import android.text.TextUtils; import com.alphawallet.app.entity.ContractType; +import com.alphawallet.app.entity.EasAttestation; +import com.alphawallet.app.entity.NetworkInfo; +import com.alphawallet.app.entity.attestation.AttestationImport; +import com.alphawallet.app.repository.entity.RealmAttestation; import com.alphawallet.app.repository.entity.RealmToken; import com.alphawallet.app.util.Utils; +import com.google.gson.Gson; import java.math.BigDecimal; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -200,4 +206,52 @@ public TokenInfo createTokenInfo(RealmToken realmItem) return new TokenInfo(realmItem.getTokenAddress(), realmItem.getName(), realmItem.getSymbol(), realmItem.getDecimals(), realmItem.isEnabled(), realmItem.getChainId()); } + + public Token createAttestation(RealmAttestation rAttn, Token token, NetworkInfo info, String wallet) + { + String jsonAttestation = Utils.toAttestationJson(Utils.parseEASAttestation(rAttn.getAttestationLink())); + if (TextUtils.isEmpty(jsonAttestation)) + { + return getLegacyAttestation(rAttn, token, info, wallet); + } + else + { + EasAttestation easAttn = new Gson().fromJson(jsonAttestation, EasAttestation.class); + String recoverAttestationSigner = AttestationImport.recoverSigner(easAttn); + TokenInfo tInfo = createAttestationTokenInfo(token, info, recoverAttestationSigner, + rAttn.getTokenAddress(), Attestation.hasSmartPassElementCheck(rAttn.getSubTitle())); + Attestation attn = new Attestation(tInfo, info.name, rAttn.getAttestationLink().getBytes(StandardCharsets.UTF_8)); + attn.setTokenWallet(wallet); + attn.loadAttestationData(rAttn, recoverAttestationSigner); + return attn; + } + } + + private TokenInfo createAttestationTokenInfo(Token token, NetworkInfo info, String recoverAttestationSigner, String tokenAddress, boolean hasSmartPassElement) + { + TokenInfo tInfo; + if (token != null) + { + tInfo = token.tokenInfo; + } + else if (hasSmartPassElement && Attestation.getKnownRootIssuers(info.chainId).contains(recoverAttestationSigner)) + { + tInfo = Attestation.getSmartPassInfo(info.chainId, tokenAddress); + } + else + { + tInfo = Attestation.getDefaultAttestationInfo(info.chainId, tokenAddress); + } + + return tInfo; + } + + private Token getLegacyAttestation(RealmAttestation rAttn, Token token, NetworkInfo info, String wallet) + { + TokenInfo tInfo = token != null ? token.tokenInfo : Attestation.getDefaultAttestationInfo(info.chainId, rAttn.getTokenAddress()); + Attestation att = new Attestation(tInfo, info.getShortName(), rAttn.getAttestation()); + att.loadAttestationData(rAttn, ""); + att.setTokenWallet(wallet); + return att; + } } diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/EventUtils.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/EventUtils.java index 39d723675a..992e4d8935 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/EventUtils.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/EventUtils.java @@ -667,7 +667,7 @@ private static boolean addTopicFilter(EventDefinition ev, EthFilter filter, Stri } else if (tokenIds.size() == 1) { - filter.addSingleTopic("0x" + TypeEncoder.encode(new Uint256(tokenIds.get(0)))); + filter.addSingleTopic(Numeric.prependHexPrefix(TypeEncoder.encode(new Uint256(tokenIds.get(0))))); } else { @@ -675,14 +675,14 @@ else if (tokenIds.size() == 1) List optionals = new ArrayList<>(); for (BigInteger uid : tokenIds) { - String entry = "0x" + TypeEncoder.encode(new Uint256(uid)); + String entry = Numeric.prependHexPrefix(TypeEncoder.encode(new Uint256(uid))); optionals.add(entry); } filter.addOptionalTopics(optionals.toArray(new String[0])); } break; case "ownerAddress": - filter.addSingleTopic("0x" + TypeEncoder.encode(new Address(attrIf.getWalletAddr()))); + filter.addSingleTopic(Numeric.prependHexPrefix(TypeEncoder.encode(new Address(attrIf.getWalletAddr())))); break; default: Attribute attr = attrIf.fetchAttribute(ev.contract, filterTopicValue); @@ -696,7 +696,7 @@ else if (tokenIds.size() == 1) else if (tokenIds.size() == 1) { TokenScriptResult.Attribute attrResult = attrIf.fetchAttrResult(tokenAddr, attr, tokenIds.get(0)); - filter.addSingleTopic("0x" + TypeEncoder.encode(new Uint256(attrResult.value))); + filter.addSingleTopic(Numeric.prependHexPrefix(TypeEncoder.encode(new Uint256(attrResult.value)))); } else { @@ -705,7 +705,7 @@ else if (tokenIds.size() == 1) for (BigInteger uid : tokenIds) { TokenScriptResult.Attribute attrResult = attrIf.fetchAttrResult(tokenAddr, attr, uid); - String entry = "0x" + TypeEncoder.encode(new Uint256(attrResult.value)); + String entry = Numeric.prependHexPrefix(TypeEncoder.encode(new Uint256(attrResult.value))); optionals.add(entry); } filter.addOptionalTopics(optionals.toArray(new String[0])); diff --git a/app/src/main/java/com/alphawallet/app/interact/FindDefaultNetworkInteract.java b/app/src/main/java/com/alphawallet/app/interact/FindDefaultNetworkInteract.java index becfac82af..fc65463c76 100644 --- a/app/src/main/java/com/alphawallet/app/interact/FindDefaultNetworkInteract.java +++ b/app/src/main/java/com/alphawallet/app/interact/FindDefaultNetworkInteract.java @@ -1,5 +1,6 @@ package com.alphawallet.app.interact; +import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.entity.NetworkInfo; diff --git a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java index 7fdd053709..1462cc6e40 100644 --- a/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java +++ b/app/src/main/java/com/alphawallet/app/repository/EthereumNetworkBase.java @@ -848,6 +848,12 @@ public NetworkInfo getNetworkByChain(long chainId) return networkMap.get(chainId); } + // Static variant to replace static in the other EthereumNetworkBase + public static NetworkInfo getNetwork(long chainId) + { + return networkMap.get(chainId); + } + // fetches the last transaction nonce; if it's identical to the last used one then increment by one // to ensure we don't get transaction replacement @Override diff --git a/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java b/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java index 19228c3d7c..5611ff445b 100644 --- a/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java +++ b/app/src/main/java/com/alphawallet/app/repository/KeyProvider.java @@ -43,4 +43,8 @@ public interface KeyProvider String getBlockPiCypressKey(); String getBlockNativeKey(); + + String getSmartPassKey(); + + String getSmartPassDevKey(); } diff --git a/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java b/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java index db6e14e05a..b8d611dc4c 100644 --- a/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java +++ b/app/src/main/java/com/alphawallet/app/repository/KeyProviderJNIImpl.java @@ -48,4 +48,8 @@ public KeyProviderJNIImpl() public native String getBlockPiCypressKey(); public native String getBlockNativeKey(); + + public native String getSmartPassKey(); + + public native String getSmartPassDevKey(); } 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 b58748186e..538893dc1d 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokenRepository.java @@ -106,18 +106,23 @@ public class TokenRepository implements TokenRepositoryType { public TokenRepository( EthereumNetworkRepositoryType ethereumNetworkRepository, TokenLocalSource localSource, - OkHttpClient okClient, Context context, TickerService tickerService) { this.ethereumNetworkRepository = ethereumNetworkRepository; this.localSource = localSource; this.ethereumNetworkRepository.addOnChangeDefaultNetwork(this::buildWeb3jClient); - this.okClient = okClient; this.context = context; this.tickerService = tickerService; web3jNodeServers = new ConcurrentHashMap<>(); currentAddress = ethereumNetworkRepository.getCurrentWalletAddress(); + + okClient = new OkHttpClient.Builder() + .connectTimeout(C.CONNECT_TIMEOUT * 4, TimeUnit.SECONDS) //events can take longer to render + .connectTimeout(C.READ_TIMEOUT * 4, TimeUnit.SECONDS) + .writeTimeout(C.LONG_WRITE_TIMEOUT, TimeUnit.SECONDS) + .retryOnConnectionFailure(true) + .build(); } private void buildWeb3jClient(NetworkInfo networkInfo) @@ -417,13 +422,6 @@ public Single update(String contractAddr, long chainId, ContractType private Single tokenInfoFromOKLinkService(String contractAddr) { - OkHttpClient okClient = new OkHttpClient.Builder() - .connectTimeout(C.CONNECT_TIMEOUT * 3, TimeUnit.SECONDS) //events can take longer to render - .connectTimeout(C.READ_TIMEOUT * 3, TimeUnit.SECONDS) - .writeTimeout(C.LONG_WRITE_TIMEOUT, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); - return Single.fromCallable(() -> OkLinkService.get(okClient).getTokenInfo(contractAddr)).observeOn(Schedulers.io()); } @@ -1011,7 +1009,16 @@ private String callSmartContractFunction( = createEthCallTransaction(wallet.address, contractAddress, encodedFunction); EthCall response = getService(network.chainId).ethCall(transaction, DefaultBlockParameterName.LATEST).send(); - return response.getValue(); + if (response.hasError() && response.getError().getMessage().equals("execution reverted")) + { + //contract does not have this function + //TODO: Handle this + return null; + } + else + { + return response.getValue(); + } } catch (InterruptedIOException|UnknownHostException|JsonParseException e) { @@ -1312,7 +1319,7 @@ public static Web3j getWeb3jService(long chainId) { OkHttpClient okClient = new OkHttpClient.Builder() .connectTimeout(C.CONNECT_TIMEOUT, TimeUnit.SECONDS) - .connectTimeout(C.READ_TIMEOUT, TimeUnit.SECONDS) + .connectTimeout(C.READ_TIMEOUT * 3, TimeUnit.SECONDS) .writeTimeout(C.LONG_WRITE_TIMEOUT, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .build(); diff --git a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java index de381614f6..30a0c0d756 100644 --- a/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java +++ b/app/src/main/java/com/alphawallet/app/repository/TokensRealmSource.java @@ -1,6 +1,5 @@ package com.alphawallet.app.repository; -import static com.alphawallet.app.service.AssetDefinitionService.getEASContract; import static com.alphawallet.app.service.TickerService.TICKER_TIMEOUT; import static com.alphawallet.app.service.TokensService.EXPIRED_CONTRACT; @@ -10,8 +9,10 @@ import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.CustomViewSettings; +import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.Wallet; +import com.alphawallet.app.entity.attestation.AttestationImport; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.opensea.AssetContract; import com.alphawallet.app.entity.tokendata.TokenGroup; @@ -36,6 +37,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -312,14 +314,10 @@ public Token fetchToken(long chainId, Wallet wallet, String address) private Token fetchAttestation(long chainId, Wallet wallet, RealmAttestation rAttn) { Token token = fetchToken(chainId, wallet, rAttn.getTokenAddress()); //<-- getTokenAddress() should be the key - //We require to - rAttn.getAttestation(); - TokenInfo tInfo = token != null ? token.tokenInfo : Utils.getDefaultAttestationInfo(chainId, rAttn.getTokenAddress()); - Attestation att = new Attestation(tInfo, ethereumNetworkRepository.getNetworkByChain(chainId).getShortName(), rAttn.getAttestation()); - att.loadAttestationData(rAttn); - att.setTokenWallet(wallet.address); - - return att; + TokenFactory tf = new TokenFactory(); + Token attn = tf.createAttestation(rAttn, token, ethereumNetworkRepository.getNetworkByChain(chainId), wallet.address); + attn.setTokenWallet(wallet.address); + return attn; } @Override @@ -351,11 +349,12 @@ public List fetchAttestations(long chainId, String walletAddress, String .like("address", databaseKey(chainId, tokenAddress) + "-*", Case.INSENSITIVE) .findAll(); + // TODO: Restore using TokenFactory for (RealmAttestation thisAttn : realmAttestations) { Attestation att = new Attestation(baseToken.tokenInfo, baseToken.getNetworkName(), thisAttn.getAttestation()); att.setTokenWallet(walletAddress); - att.loadAttestationData(thisAttn); //TODO: Store issuer, expiry dates etc in Realm + att.loadAttestationData(thisAttn, ""); //TODO: Store issuer, expiry dates etc in Realm attestations.add(att); } } @@ -463,7 +462,7 @@ private void setTokenUpdateTime(RealmToken realmToken, Token token) { realmToken.setLastTxTime(System.currentTimeMillis()); - if (realmToken.getBalance() == null || !realmToken.getBalance().equals(token.getBalanceRaw().toString())) + if (!realmToken.getBalance().equals(token.getBalanceRaw().toString())) { token.setRealmBalance(realmToken); } @@ -567,7 +566,7 @@ public boolean updateTokenBalance(Wallet wallet, Token token, BigDecimal balance if (realmToken != null) { final String currentBalance = realmToken.getBalance(); - final String newBalance = (balanceArray == null) ? balance.toString() : Utils.bigIntListToString(balanceArray, true); + final String newBalance = (balanceArray == null || balanceArray.size() == 0) ? balance.toString() : Utils.bigIntListToString(balanceArray, true); //does the token need updating? if (token.checkInfoRequiresUpdate(realmToken)) @@ -592,11 +591,19 @@ public boolean updateTokenBalance(Wallet wallet, Token token, BigDecimal balance Timber.tag(TAG).d("Zero out ERC721 balance: %s :%s", realmToken.getName(), token.getAddress()); balanceChanged = true; } - else if (!newBalance.equals(currentBalance) || !checkEthToken(realm, token)) + else if (!TextUtils.isEmpty(newBalance) && (!newBalance.equals(currentBalance) || !checkEthToken(realm, token))) { realm.executeTransaction(r -> { realmToken.setBalance(newBalance); - if (token.isEthereum()) updateEthToken(r, token, newBalance); + if (token.isEthereum()) + { + updateEthToken(r, token, newBalance); + } + if (currentBalance.equals("0") && !realmToken.isVisibilityChanged()) + { + realmToken.setEnabled(true); + realmToken.setUpdateTime(System.currentTimeMillis()); + } }); Timber.tag(TAG).d("Update Token Balance: %s :%s", realmToken.getName(), token.getAddress()); balanceChanged = true; @@ -1271,7 +1278,7 @@ public void updateEthTickers(Map ethTickers) } catch (Exception e) { - // + Timber.w(e); } //This will trigger an update of the holder @@ -1477,17 +1484,6 @@ private boolean writeTickerToRealm(Realm realm, final TokenTicker ticker, long c realmItem = realm.createObject(RealmTokenTicker.class, databaseKey); realmItem.setCreatedTime(ticker.updateTime); } - else - { - //compare old ticker to see if we need an update - if (realmItem.getCurrencySymbol().equals(ticker.priceSymbol) && realmItem.getPrice().equals(ticker.price) - && realmItem.getPercentChange24h().equals(ticker.percentChange24h)) - { - //no update, but update the received time - realmItem.setUpdatedTime(ticker.updateTime); - return false; - } - } realmItem.setPercentChange24h(ticker.percentChange24h); realmItem.setPrice(ticker.price); diff --git a/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java b/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java index 38d515e85c..e17543b7c3 100644 --- a/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java +++ b/app/src/main/java/com/alphawallet/app/repository/entity/RealmAttestation.java @@ -104,10 +104,15 @@ public void setAttestation(byte[] attestation) this.attestation = Base64.encodeToString(attestation, Base64.DEFAULT); } - public void setAttestation(String attestation) + public void setAttestationLink(String attestation) { //should be in hex - this.attestation = Base64.encodeToString(Numeric.hexStringToByteArray(attestation), Base64.DEFAULT); + this.attestation = attestation; + } + + public String getAttestationLink() + { + return attestation; } public byte[] getAttestation() @@ -119,12 +124,6 @@ public boolean supportsChain(List networkFilters) { HashSet knownChains = new HashSet<>(Utils.longListToArray(chains)); - //validate attestation - if (TextUtils.isEmpty(id)) - { - return false; - } - for (long chainId : knownChains) { if (networkFilters.contains(chainId)) diff --git a/app/src/main/java/com/alphawallet/app/repository/entity/RealmToken.java b/app/src/main/java/com/alphawallet/app/repository/entity/RealmToken.java index c753908748..746719e358 100644 --- a/app/src/main/java/com/alphawallet/app/repository/entity/RealmToken.java +++ b/app/src/main/java/com/alphawallet/app/repository/entity/RealmToken.java @@ -10,7 +10,8 @@ import io.realm.RealmObject; import io.realm.annotations.PrimaryKey; -public class RealmToken extends RealmObject { +public class RealmToken extends RealmObject +{ @PrimaryKey private String address; private String name; @@ -30,31 +31,38 @@ public class RealmToken extends RealmObject { private boolean visibilityChanged; private String erc1155BlockRead; - public int getDecimals() { + public int getDecimals() + { return decimals; } - public void setDecimals(int decimals) { + public void setDecimals(int decimals) + { this.decimals = decimals; -} + } - public String getSymbol() { + public String getSymbol() + { return symbol; } - public void setSymbol(String symbol) { + public void setSymbol(String symbol) + { this.symbol = symbol; } - public String getName() { + public String getName() + { return name; } - public void setName(String name) { + public void setName(String name) + { this.name = name; } - public String getTokenAddress() { + public String getTokenAddress() + { String tAddress = address; if (tAddress.contains(".")) //base chain { @@ -70,16 +78,27 @@ else if (tAddress.contains("-")) } } - public long getUpdateTime() { + public long getUpdateTime() + { return addedTime; } - public void setUpdateTime(long addedTime) { - this.addedTime = addedTime; + // + public void setUpdateTime(long addedTime) + { + this.updatedTime = addedTime; } - public String getBalance() { - return balance; + public String getBalance() + { + if (TextUtils.isEmpty(balance)) + { + return "0"; + } + else + { + return balance; + } } public void setBalance(String balance) @@ -97,13 +116,18 @@ public long getBalanceUpdateTime() return this.updatedTime; } - public boolean getEnabled() { + public boolean getEnabled() + { return isEnabled; } - public boolean isEnabled() { return isEnabled; } + public boolean isEnabled() + { + return isEnabled; + } - public void setEnabled(boolean isEnabled) { + public void setEnabled(boolean isEnabled) + { this.isEnabled = isEnabled; } @@ -148,10 +172,21 @@ public void setLastBlock(long lastBlockCheck) { this.lastBlockRead = lastBlockCheck; } - public long getLastBlock() { return lastBlockRead; } - public long getChainId() { return chainId; } - public void setChainId(long chainId) { this.chainId = chainId; } + public long getLastBlock() + { + return lastBlockRead; + } + + public long getChainId() + { + return chainId; + } + + public void setChainId(long chainId) + { + this.chainId = chainId; + } public long getLastTxTime() { diff --git a/app/src/main/java/com/alphawallet/app/service/AWHttpService.java b/app/src/main/java/com/alphawallet/app/service/AWHttpService.java index 6170a54bcd..ab9dbd80f2 100644 --- a/app/src/main/java/com/alphawallet/app/service/AWHttpService.java +++ b/app/src/main/java/com/alphawallet/app/service/AWHttpService.java @@ -125,9 +125,11 @@ protected InputStream performIO(String request) throws IOException requestBody = RequestBody.create("", MEDIA_TYPE_TEXT); } - Headers headers = buildHeaders(); okhttp3.Request httpRequest = - new okhttp3.Request.Builder().url(url).headers(headers).post(requestBody).build(); + new okhttp3.Request.Builder() + .url(url) + .headers(buildHeaders()) + .post(requestBody).build(); okhttp3.Response response = null; 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 d700864069..e52265b998 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -2,9 +2,6 @@ import static com.alphawallet.app.repository.TokenRepository.getWeb3jService; import static com.alphawallet.app.repository.TokensRealmSource.IMAGES_DB; -import static com.alphawallet.ethereum.EthereumNetworkBase.ARBITRUM_MAIN_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.MAINNET_ID; -import static com.alphawallet.ethereum.EthereumNetworkBase.SEPOLIA_TESTNET_ID; import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_CURRENT_SCHEMA; import static com.alphawallet.token.tools.TokenDefinition.TOKENSCRIPT_REPO_SERVER; import static com.alphawallet.token.tools.TokenDefinition.UNCHANGED_SCRIPT; @@ -21,7 +18,6 @@ import android.util.Pair; import androidx.annotation.Keep; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; import androidx.lifecycle.MutableLiveData; @@ -33,11 +29,10 @@ import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.QueryResponse; import com.alphawallet.app.entity.TokenLocator; -import com.alphawallet.app.entity.attestation.SchemaRecord; +import com.alphawallet.app.entity.attestation.AttestationImport; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.tokens.Attestation; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.entity.tokenscript.EventUtils; import com.alphawallet.app.entity.tokenscript.TokenScriptFile; import com.alphawallet.app.entity.tokenscript.TokenscriptFunction; @@ -49,11 +44,7 @@ import com.alphawallet.app.ui.HomeActivity; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.HomeViewModel; -import com.alphawallet.app.web3j.StructuredDataEncoder; -import com.alphawallet.ethereum.EthereumNetworkBase; -import com.alphawallet.ethereum.NetworkInfo; import com.alphawallet.token.entity.ActionModifier; -import com.alphawallet.token.entity.AttestationDefinition; import com.alphawallet.token.entity.Attribute; import com.alphawallet.token.entity.AttributeInterface; import com.alphawallet.token.entity.ContractAddress; @@ -78,18 +69,9 @@ import org.jetbrains.annotations.NotNull; import org.web3j.abi.DefaultFunctionEncoder; 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.Bool; -import org.web3j.abi.datatypes.DynamicBytes; import org.web3j.abi.datatypes.Function; import org.web3j.abi.datatypes.Type; -import org.web3j.abi.datatypes.Utf8String; -import org.web3j.abi.datatypes.generated.Bytes32; -import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.crypto.Keys; -import org.web3j.crypto.Sign; import org.web3j.protocol.Web3j; import org.web3j.protocol.core.methods.request.EthFilter; import org.web3j.protocol.core.methods.response.EthBlock; @@ -110,7 +92,6 @@ import java.math.BigInteger; import java.net.HttpURLConnection; import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -840,7 +821,7 @@ private boolean checkReadPermission() == PackageManager.PERMISSION_GRANTED; } - private TokenDefinition getDefinition(String tsKey) + public TokenDefinition getDefinition(String tsKey) { TokenDefinition result = null; String[] elements = tsKey.split("-"); @@ -2178,7 +2159,7 @@ public Single>> fetchFunctionMap(Token token, @NotN { //get required Attribute Results for this tokenId & selection List requiredAttributeNames = selection.getRequiredAttrs(); - Map idAttrResults = getAttributeResultsForTokenIds(attrResults, requiredAttributeNames, tokenId); + Map idAttrResults = getAttributeResultsForTokenIds(td, attrResults, requiredAttributeNames, tokenId); addIntrinsicAttributes(idAttrResults, token, tokenId); //adding intrinsic attributes eg ownerAddress, tokenId, contractAddress //Now evaluate the selection @@ -2243,15 +2224,21 @@ public String checkFunctionDenied(Token token, String actionName, List getAttributeResultsForTokenIds(Map> attrResults, List requiredAttributeNames, + private Map getAttributeResultsForTokenIds(TokenDefinition td, Map> attrResults, List requiredAttributeNames, BigInteger tokenId) { Map results = new HashMap<>(); - if (!attrResults.containsKey(tokenId)) return results; //check values for (String attributeName : requiredAttributeNames) { - results.put(attributeName, attrResults.get(tokenId).get(attributeName)); + BigInteger useTokenId = td.useZeroForTokenIdAgnostic(attributeName, tokenId); + + if (!attrResults.containsKey(useTokenId)) + { + continue; + } + + results.put(attributeName, attrResults.get(useTokenId).get(attributeName)); } return results; @@ -2266,14 +2253,15 @@ private Map> getRequiredAtt { Attribute attr = td.attributes.get(attrName); if (attr == null) continue; - TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, tokenId, td, this, ViewType.VIEW).blockingGet(); + BigInteger useTokenId = td.useZeroForTokenIdAgnostic(attrName, tokenId); + TokenScriptResult.Attribute attrResult = tokenscriptUtility.fetchAttrResult(token, attr, useTokenId, td, this, ViewType.VIEW).blockingGet(); if (attrResult != null) { - Map tokenIdMap = resultSet.get(tokenId); + Map tokenIdMap = resultSet.get(useTokenId); if (tokenIdMap == null) { tokenIdMap = new HashMap<>(); - resultSet.put(tokenId, tokenIdMap); + resultSet.put(useTokenId, tokenIdMap); } tokenIdMap.put(attrName, attrResult); } @@ -2744,7 +2732,7 @@ public List getAttestationAttrs(Token token, TSActi DefaultFunctionEncoder dfe = new DefaultFunctionEncoder(); EasAttestation easAttestation = att.getEasAttestation(); List inputParam = Collections.singletonList(easAttestation.getAttestationCore()); - String coreAttestationHex = "0x" + dfe.encodeParameters(inputParam).substring(0x40); + String coreAttestationHex = Numeric.prependHexPrefix(dfe.encodeParameters(inputParam).substring(0x40)); attrs.add(new TokenScriptResult.Attribute("attestation", "attestation", BigInteger.ZERO, coreAttestationHex)); //also require signature String signatureBytes = Numeric.toHexString(easAttestation.getSignatureBytes()); @@ -3111,293 +3099,13 @@ private void deleteAWRealm() } } - public Attestation validateAttestation(String attestation, TokenInfo tInfo) + public Function generateTransactionFunction(Token token, BigInteger tokenId, TokenDefinition td, FunctionDefinition fd) { - TokenDefinition td = getDefinition(getTSDataKeyTemp(tInfo.chainId, tInfo.address));//getDefinition(tInfo.chainId, tInfo.address); - Attestation att = null; - - if (td != null) - { - NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(tInfo.chainId); - att = new Attestation(tInfo, networkInfo.name, Numeric.hexStringToByteArray(attestation)); - att.setTokenWallet(tokensService.getCurrentAddress()); - - //call validation function and get details - AttestationDefinition definitionAtt = td.getAttestation(); - //can we get the details? - - if (definitionAtt != null && definitionAtt.function != null) - { - //pull return type - FunctionDefinition fd = definitionAtt.function; - //add attestation to attr map - //call function - org.web3j.abi.datatypes.Function transaction = tokenscriptUtility.generateTransactionFunction(att, BigInteger.ZERO, td, fd, this); - transaction = new Function(fd.method, transaction.getInputParameters(), td.getAttestationReturnTypes()); //set return types - - //call and handle result - String result = tokenscriptUtility.callSmartContract(tInfo.chainId, tInfo.address, transaction); - - //break down result - List values = FunctionReturnDecoder.decode(result, transaction.getOutputParameters()); - - //interpret these values - att.handleValidation(td.getValidation(values)); - } - } - - return att; + return tokenscriptUtility.generateTransactionFunction(token, tokenId, td, fd, this); } - public Attestation validateAttestation(EasAttestation attestation, String importedAttestation) + public String callSmartContract(long chainId, String contractAddress, Function function) { - String recoverAttestationSigner = recoverSigner(attestation); - - //1. Validate signer via key attestation service (using UID). - //call validate - SchemaRecord schemaRecord = fetchSchemaRecord(attestation.getChainId(), getKeySchemaUID(attestation.getChainId())); - boolean attestationValid = checkAttestationIssuer(schemaRecord, attestation.getChainId(), recoverAttestationSigner); - - //2. Decode the ABI encoded payload to pull out the info. ABI Decode the schema bytes - //initially we need a hardcoded schema - this should be fetched from the schema record EAS contract - //fetch the schema of the attestation - SchemaRecord attestationSchema = fetchSchemaRecord(attestation.getChainId(), attestation.schema); - //convert into functionDecode - List names = new ArrayList<>(); - List values = decodeAttestationData(attestation.data, attestationSchema.schema, names); - - NetworkInfo networkInfo = EthereumNetworkBase.getNetworkByChain(attestation.getChainId()); - - TokenInfo tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId(), getEASContract(attestation.chainId)); - Attestation localAttestation = new Attestation(tInfo, networkInfo.name, importedAttestation.getBytes(StandardCharsets.UTF_8)); - localAttestation.handleEASAttestation(attestation, names, values, attestationValid); - - String collectionHash = localAttestation.getAttestationCollectionId(); - - //Now regenerate with the correct collectionId - tInfo = Utils.getDefaultAttestationInfo(attestation.getChainId(), collectionHash); - localAttestation = new Attestation(tInfo, networkInfo.name, importedAttestation.getBytes(StandardCharsets.UTF_8)); - localAttestation.handleEASAttestation(attestation, names, values, attestationValid); - localAttestation.setTokenWallet(tokensService.getCurrentAddress()); - return localAttestation; - } - - private String getDataValue(String key, List names, List values) - { - Map valueMap = new HashMap<>(); - for (int index = 0; index < names.size(); index++) - { - String name = names.get(index); - Type type = values.get(index); - valueMap.put(name, type.toString()); - } - - return valueMap.get(key); - } - - private List decodeAttestationData(String attestationData, @NonNull String decodeSchema, List names) - { - List> returnTypes = new ArrayList>(); - //build decoder - String[] typeData = decodeSchema.split(","); - for (String typeElement : typeData) - { - String[] data = typeElement.split(" "); - String type = data[0]; - String name = data[1]; - if (type.startsWith("uint") || type.startsWith("int")) - { - type = "uint"; - } - else if (type.startsWith("bytes") && !type.equals("bytes")) - { - type = "bytes32"; - } - - TypeReference tRef = null; - - switch (type) - { - case "uint": - tRef = new TypeReference() { }; - break; - case "address": - tRef = new TypeReference
() { }; - break; - case "bytes32": - tRef = new TypeReference() { }; - break; - case "string": - tRef = new TypeReference() { }; - break; - case "bytes": - tRef = new TypeReference() { }; - break; - case "bool": - tRef = new TypeReference() { }; - break; - default: - break; - } - - if (tRef != null) - { - returnTypes.add(tRef); - } - else - { - Timber.e("Unhandled type!"); - returnTypes.add(new TypeReference() { }); - } - - names.add(name); - } - - //decode the schema and populate the Attestation element - return FunctionReturnDecoder.decode(attestationData, org.web3j.abi.Utils.convert(returnTypes)); - } - - private SchemaRecord fetchSchemaRecord(long chainId, String schemaUID) - { - //1. Resolve UID. For now, just use default: This should be on a switch for chains - String globalResolver = getEASSchemaContract(chainId); - - //format transaction to get key resolver - Function getKeyResolver2 = new Function("getSchema", - Collections.singletonList(new Bytes32(Numeric.hexStringToByteArray(schemaUID))), - Collections.singletonList(new TypeReference() {})); - - String result = tokenscriptUtility.callSmartContract(chainId, globalResolver, getKeyResolver2); - List values = FunctionReturnDecoder.decode(result, getKeyResolver2.getOutputParameters()); - - return (SchemaRecord)values.get(0); - } - - private boolean checkAttestationIssuer(SchemaRecord schemaRecord, long chainId, String signer) - { - String rootKeyUID = getDefaultRootKeyUID(chainId); - //pull the key resolver - Address resolverAddr = schemaRecord.resolver; - //call the resolver to test key validity - Function validateKey = new Function("validateSignature", - Arrays.asList((new Bytes32(Numeric.hexStringToByteArray(rootKeyUID))), - new Address(signer)), - Collections.singletonList(new TypeReference() {})); - - String result = tokenscriptUtility.callSmartContract(chainId, resolverAddr.getValue(), validateKey); - List values = FunctionReturnDecoder.decode(result, validateKey.getOutputParameters()); - return ((Bool)values.get(0)).getValue(); - } - - private String recoverSigner(EasAttestation attestation) - { - String recoveredAddress = ""; - - try - { - StructuredDataEncoder dataEncoder = new StructuredDataEncoder(attestation.getEIP712Attestation()); - byte[] hash = dataEncoder.hashStructuredData(); - byte[] r = Numeric.hexStringToByteArray(attestation.getR()); - byte[] s = Numeric.hexStringToByteArray(attestation.getS()); - byte v = (byte)(attestation.getV() & 0xFF); - - Sign.SignatureData sig = new Sign.SignatureData(v, r, s); - - BigInteger key = Sign.signedMessageHashToKey(hash, sig); - recoveredAddress = "0x" + Keys.getAddress(key); - } - catch (Exception e) - { - e.printStackTrace(); - } - - return recoveredAddress; - } - - // NB Java 11 doesn't have support for switching on a 'long' :( - public static String getEASContract(long chainId) - { - if (chainId == MAINNET_ID) - { - return "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587"; - } - else if (chainId == ARBITRUM_MAIN_ID) - { - return "0xbD75f629A22Dc1ceD33dDA0b68c546A1c035c458"; - } - else if (chainId == SEPOLIA_TESTNET_ID) - { - return "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"; - } - else - { - //Support Optimism Goerli (0xC2679fBD37d54388Ce493F1DB75320D236e1815e) - return ""; - } - } - - private String getEASSchemaContract(long chainId) - { - if (chainId == MAINNET_ID) - { - return "0xA7b39296258348C78294F95B872b282326A97BDF"; - } - else if (chainId == ARBITRUM_MAIN_ID) - { - return "0xA310da9c5B885E7fb3fbA9D66E9Ba6Df512b78eB"; - } - else if (chainId == SEPOLIA_TESTNET_ID) - { - return "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0"; - } - else - { - //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) - return ""; - } - } - - //UID of schema used for keys on each chain - the resolver is tied to this UID - private String getKeySchemaUID(long chainId) - { - if (chainId == MAINNET_ID) - { - return ""; - } - else if (chainId == ARBITRUM_MAIN_ID) - { - return "0x5f0437f7c1db1f8e575732ca52cc8ad899b3c9fe38b78b67ff4ba7c37a8bf3b4"; - } - else if (chainId == SEPOLIA_TESTNET_ID) - { - return "0x4455598d3ec459c4af59335f7729fea0f50ced46cb1cd67914f5349d44142ec1"; - } - else - { - //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) - return ""; - } - } - - // If not specified - private String getDefaultRootKeyUID(long chainId) - { - if (chainId == MAINNET_ID) - { - return ""; - } - else if (chainId == ARBITRUM_MAIN_ID) - { - return "0xe5c2bfd98a1b35573610b4e5a367bbcb5c736e42508a33fd6046bad63eaf18f9"; - } - else if (chainId == SEPOLIA_TESTNET_ID) - { - return "0xee99de42f544fa9a47caaf8d4a4426c1104b6d7a9df7f661f892730f1b5b1e23"; - } - else - { - //Support Optimism Goerli (0x7b24C7f8AF365B4E308b6acb0A7dfc85d034Cb3f) - return ""; - } + return tokenscriptUtility.callSmartContract(chainId, contractAddress, function); } } diff --git a/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java b/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java index cd36d346b6..dacbde0e63 100644 --- a/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java +++ b/app/src/main/java/com/alphawallet/app/service/BlockNativeGasAPI.java @@ -150,14 +150,6 @@ private BigInteger elementToWei(String value) } } - /* - public EIP1559FeeOracleResult(BigInteger maxFee, BigInteger maxPriority, BigInteger base) - { - maxFeePerGas = fixGasPriceReturn(maxFee); // Some chains (eg Phi) have a gas price lower than 1Gwei. - maxPriorityFeePerGas = fixGasPriceReturn(maxPriority); - baseFee = base; - } - */ public EIP1559FeeOracleResult getFeeOracleResult(BigInteger baseFee) { return new EIP1559FeeOracleResult(getMaxFeePerGasWei(), getMaxPriorityFeePerGasWei(), baseFee); diff --git a/app/src/main/java/com/alphawallet/app/service/GasService.java b/app/src/main/java/com/alphawallet/app/service/GasService.java index 1c7a8b83f6..7754b272f1 100644 --- a/app/src/main/java/com/alphawallet/app/service/GasService.java +++ b/app/src/main/java/com/alphawallet/app/service/GasService.java @@ -19,7 +19,6 @@ import com.alphawallet.app.entity.GasPriceSpread; import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.entity.SuggestEIP1559Kt; -import com.alphawallet.app.entity.TXSpeed; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.repository.EthereumNetworkBase; @@ -43,19 +42,14 @@ import org.web3j.protocol.http.HttpService; import org.web3j.tx.gas.ContractGasProvider; -import java.io.IOException; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.Single; -import io.reactivex.SingleSource; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; -import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import io.realm.Realm; import okhttp3.OkHttpClient; @@ -88,8 +82,6 @@ public class GasService implements ContractGasProvider private final String ETHERSCAN_API_KEY; private final String POLYGONSCAN_API_KEY; private boolean keyFail; - private static final List noEIP1559 = new ArrayList<>(); - @Nullable private Disposable gasFetchDisposable; @@ -113,11 +105,9 @@ public GasService(EthereumNetworkRepositoryType networkRepository, OkHttpClient public void startGasPriceCycle(long chainId) { updateChainId(chainId); - if (gasFetchDisposable == null || gasFetchDisposable.isDisposed()) - { - gasFetchDisposable = Observable.interval(0, FETCH_GAS_PRICE_INTERVAL_SECONDS, TimeUnit.SECONDS) - .doOnNext(l -> fetchCurrentGasPrice()).subscribe(); - } + if (gasFetchDisposable != null && !gasFetchDisposable.isDisposed()) gasFetchDisposable.dispose(); + gasFetchDisposable = Observable.interval(0, FETCH_GAS_PRICE_INTERVAL_SECONDS, TimeUnit.SECONDS) + .doOnNext(l -> fetchCurrentGasPrice()).subscribe(); } public void stopGasPriceCycle() @@ -152,20 +142,17 @@ private void fetchCurrentGasPrice() .observeOn(AndroidSchedulers.mainThread()) .subscribe(updated -> { Timber.d("Updated gas prices: %s", updated); - }, Throwable::printStackTrace) + }, Throwable::printStackTrace) .isDisposed(); //also update EIP1559 if required and we haven't previously determined there's no EIP1559 support - if (!noEIP1559.contains(currentChainId)) - { - getEIP1559FeeStructure() - .map(result -> updateEIP1559Realm(result, currentChainId)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(r -> { - if (!r) Timber.d("Fail to update fees"); - }, this::handleError).isDisposed(); - } + getEIP1559FeeStructure() + .map(result -> updateEIP1559Realm(result, currentChainId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(r -> { + if (!r) Timber.d("Fail to update fees"); + }, this::handleError).isDisposed(); } @Override @@ -487,7 +474,7 @@ public static BigInteger getDefaultGasLimit(Token token, Web3Transaction tx) public Single getChainFeeHistory(int blockCount, String lastBlock, String rewardPercentiles) { //TODO: Replace once Web3j fully supports EIP1559 - String requestJSON = FEE_HISTORY.replace(BLOCK_COUNT, ("0x" + Long.toHexString(blockCount))).replace(NEWEST_BLOCK, lastBlock) + String requestJSON = FEE_HISTORY.replace(BLOCK_COUNT, (Numeric.prependHexPrefix(Long.toHexString(blockCount)))).replace(NEWEST_BLOCK, lastBlock) .replace(REWARD_PERCENTILES, rewardPercentiles); RequestBody requestBody = RequestBody.create(requestJSON, HttpService.JSON_MEDIA_TYPE); @@ -514,7 +501,6 @@ public Single getChainFeeHistory(int blockCount, String lastBlock, S catch (org.json.JSONException j) { Timber.e("Note: " + info.getShortName() + " does not appear to have EIP1559 support"); - noEIP1559.add(info.chainId); } catch (Exception e) { diff --git a/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java b/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java index 5055a12e1b..e17f91714a 100644 --- a/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java +++ b/app/src/main/java/com/alphawallet/app/service/KeystoreAccountService.java @@ -142,7 +142,7 @@ private String extractAddressFromStore(String store) throws Exception try { JSONObject jsonObject = new JSONObject(store); - return "0x" + Numeric.cleanHexPrefix(jsonObject.getString("address")); + return Numeric.prependHexPrefix(jsonObject.getString("address")); } catch (JSONException ex) { @@ -430,7 +430,7 @@ public Single fetchAccounts() { String fName = f.getName(); int index = fName.lastIndexOf("-"); - String address = "0x" + fName.substring(index + 1); + String address = Numeric.prependHexPrefix(fName.substring(index + 1)); if (Utils.isAddressValid(address)) { String d = fName.substring(5, index - 1).replace("T", " ").substring(0, 23); diff --git a/app/src/main/java/com/alphawallet/app/service/SignatureLookupService.java b/app/src/main/java/com/alphawallet/app/service/SignatureLookupService.java index 57e3bebdcf..86a567bdee 100644 --- a/app/src/main/java/com/alphawallet/app/service/SignatureLookupService.java +++ b/app/src/main/java/com/alphawallet/app/service/SignatureLookupService.java @@ -3,9 +3,10 @@ import com.alphawallet.app.C; import com.alphawallet.app.entity.Result; import com.alphawallet.app.util.JsonUtils; -import com.alphawallet.token.tools.Numeric; import com.google.gson.Gson; +import org.web3j.utils.Numeric; + import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -47,7 +48,7 @@ public String getTextSignature(String response) private String getFirstFourBytes(String payload) { - return ("0x" + Numeric.cleanHexPrefix(payload)).substring(0, 10); + return (Numeric.prependHexPrefix(payload)).substring(0, 10); } private String executeRequest(Request request) diff --git a/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java b/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java index 1d4e70f201..5d46f986c9 100644 --- a/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/AddTokenActivity.java @@ -329,7 +329,7 @@ private void onSave() HashSet chainsNotEnabled = new HashSet<>(); for (TokenCardMeta token : selected) { - NetworkInfo info = viewModel.ethereumNetworkRepository().getNetworkByChain(token.getChain()); + NetworkInfo info = viewModel.getNetworkInfo(token.getChain()); if (!activeChains.contains(info.chainId)) { chainsNotEnabled.add(info.chainId); @@ -550,7 +550,7 @@ private List getSelectedChains() for (TokenCardMeta token : adapter.getSelected()) { - NetworkInfo info = viewModel.ethereumNetworkRepository().getNetworkByChain(token.getChain()); + NetworkInfo info = viewModel.getNetworkInfo(token.getChain()); selectedChains.add(info.chainId); } diff --git a/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java b/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java index ad88271363..7cce28ba11 100644 --- a/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/DappBrowserFragment.java @@ -619,6 +619,7 @@ public void comeIntoFocus() startBalanceListener(); viewModel.updateGasPrice(activeNetwork.chainId); } + viewModel.getTokenService().stopUpdateCycle(); } addressBar.leaveEditMode(); } @@ -630,6 +631,7 @@ public void leaveFocus() addressBar.leaveFocus(); if (viewModel != null) viewModel.stopBalanceUpdate(); stopBalanceListener(); + viewModel.getTokenService().startUpdateCycle(); } /** diff --git a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java index 74fb0afe53..ab6d6baa93 100644 --- a/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/FunctionActivity.java @@ -12,6 +12,7 @@ import android.os.Looper; import android.text.TextUtils; import android.util.Base64; +import android.util.Pair; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -27,6 +28,7 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; import com.alphawallet.app.entity.DApp; +import com.alphawallet.app.entity.GasEstimate; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; import com.alphawallet.app.entity.TransactionReturn; @@ -122,7 +124,6 @@ private void initViews() { long chainId = getIntent().getLongExtra(C.EXTRA_CHAIN_ID, EthereumNetworkBase.MAINNET_ID); token = resolveAssetToken(wallet, chainId); - //token = viewModel.getTokenService().getToken(wallet.address, chainId, address); if (token == null) { @@ -256,8 +257,11 @@ private void onError(Throwable throwable) private void onAttr(TokenScriptResult.Attribute attribute) { //is the attr incomplete? - Timber.d("ATTR/FA: " + attribute.id + " (" + attribute.name + ")" + " : " + attribute.text); - TokenScriptResult.addPair(attrs, attribute); + if (!TextUtils.isEmpty(attribute.id)) + { + Timber.d("ATTR/FA: " + attribute.id + " (" + attribute.name + ")" + " : " + attribute.text); + TokenScriptResult.addPair(attrs, attribute); + } } private void fillEmpty() @@ -306,6 +310,7 @@ private void initViewModel() .get(TokenFunctionViewModel.class); viewModel.invalidAddress().observe(this, this::errorInvalidAddress); viewModel.insufficientFunds().observe(this, this::errorInsufficientFunds); + viewModel.gasEstimateError().observe(this, this::estimateError); viewModel.gasEstimateComplete().observe(this, this::checkConfirm); viewModel.transactionFinalised().observe(this, this::txWritten); viewModel.transactionError().observe(this, this::txError); @@ -411,16 +416,19 @@ private void completeAction() { //open action sheet after we determine the gas limit Web3Transaction web3Tx = viewModel.handleFunction(action, tokenId, token, this); - if (web3Tx.gasLimit.equals(BigInteger.ZERO)) - { - calculateEstimateDialog(); - //get gas estimate - viewModel.estimateGasLimit(web3Tx, token.tokenInfo.chainId); - } - else + if (web3Tx != null) { - //go straight to confirmation - checkConfirm(web3Tx); + if (web3Tx.gasLimit.equals(BigInteger.ZERO)) + { + calculateEstimateDialog(); + //get gas estimate + viewModel.estimateGasLimit(web3Tx, token.tokenInfo.chainId); + } + else + { + //go straight to confirmation + checkConfirm(web3Tx); + } } viewModel.getAssetDefinitionService().clearResultMap(); } @@ -434,7 +442,7 @@ private void checkConfirm(Web3Transaction w3tx) { if (w3tx.gasLimit.equals(BigInteger.ZERO)) { - estimateError(w3tx); + estimateError(new Pair<>(new GasEstimate(BigInteger.ZERO, "Gas Estimate Zero"), w3tx)); } else if (confirmationDialog == null || !confirmationDialog.isShowing()) { @@ -457,24 +465,28 @@ private void calculateEstimateDialog() alertDialog.show(); } - private void estimateError(final Web3Transaction w3tx) + private void estimateError(Pair estimate) { + if (alertDialog != null && alertDialog.isShowing()) alertDialog.dismiss(); if (alertDialog != null && alertDialog.isShowing()) alertDialog.dismiss(); alertDialog = new AWalletAlertDialog(this); alertDialog.setIcon(WARNING); - alertDialog.setTitle(R.string.confirm_transaction); - alertDialog.setMessage(R.string.error_transaction_may_fail); - alertDialog.setButtonText(R.string.button_ok); + alertDialog.setTitle(estimate.first.hasError() ? + R.string.dialog_title_gas_estimation_failed : + R.string.confirm_transaction + ); + String message = estimate.first.hasError() ? + getString(R.string.dialog_message_gas_estimation_failed, estimate.first.getError()) : + getString(R.string.error_transaction_may_fail); + alertDialog.setMessage(message); + alertDialog.setButtonText(R.string.action_proceed); alertDialog.setSecondaryButtonText(R.string.action_cancel); alertDialog.setButtonListener(v -> { + Web3Transaction w3tx = estimate.second; BigInteger gasEstimate = GasService.getDefaultGasLimit(token, w3tx); checkConfirm(new Web3Transaction(w3tx.recipient, w3tx.contract, w3tx.value, w3tx.gasPrice, gasEstimate, w3tx.nonce, w3tx.payload, w3tx.description)); }); - - alertDialog.setSecondaryButtonListener(v -> { - alertDialog.dismiss(); - }); - + alertDialog.setSecondaryButtonListener(v -> alertDialog.dismiss()); alertDialog.show(); } @@ -682,7 +694,7 @@ public void testRecoverAddressFromSignature(String message, String sig) try { BigInteger recoveredKey = Sign.signedMessageToKey(msgHash, sd); - addressRecovered = "0x" + Keys.getAddress(recoveredKey); + addressRecovered = Numeric.prependHexPrefix(Keys.getAddress(recoveredKey)); Timber.d("Recovered: %s", addressRecovered); } catch (SignatureException e) diff --git a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java index fb9357a6d5..6c3ac17b08 100644 --- a/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/HomeActivity.java @@ -23,7 +23,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -58,17 +57,18 @@ import com.alphawallet.app.entity.ContractLocator; import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.CustomViewSettings; -import com.alphawallet.app.entity.EIP681Type; import com.alphawallet.app.entity.ErrorEnvelope; import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.HomeCommsInterface; import com.alphawallet.app.entity.HomeReceiver; import com.alphawallet.app.entity.MediaLinks; -import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletPage; import com.alphawallet.app.entity.WalletType; +import com.alphawallet.app.entity.attestation.AttestationImportInterface; +import com.alphawallet.app.entity.attestation.SmartPassReturn; +import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.router.ImportTokenRouter; import com.alphawallet.app.service.NotificationService; @@ -76,7 +76,6 @@ import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; import com.alphawallet.app.ui.widget.entity.PagerCallback; import com.alphawallet.app.util.LocaleUtils; -import com.alphawallet.app.util.PermissionUtils; import com.alphawallet.app.util.UpdateUtils; import com.alphawallet.app.util.Utils; import com.alphawallet.app.viewmodel.BaseNavigationActivity; @@ -94,13 +93,11 @@ import com.alphawallet.token.tools.Numeric; import com.alphawallet.token.tools.ParseMagicLink; import com.github.florent37.tutoshowcase.TutoShowcase; -import com.google.zxing.client.android.Intents; import com.journeyapps.barcodescanner.ScanContract; import com.journeyapps.barcodescanner.ScanOptions; import net.yslibrary.android.keyboardvisibilityevent.KeyboardVisibilityEvent; -import java.net.URLDecoder; import java.util.List; import javax.inject.Inject; @@ -110,7 +107,7 @@ @AndroidEntryPoint public class HomeActivity extends BaseNavigationActivity implements View.OnClickListener, HomeCommsInterface, - FragmentMessenger, Runnable, ActionSheetCallback, LifecycleEventObserver, PagerCallback + FragmentMessenger, Runnable, ActionSheetCallback, LifecycleEventObserver, PagerCallback, AttestationImportInterface { @Inject AWWalletConnectClient awWalletConnectClient; @@ -468,11 +465,12 @@ private void setupFragmentListeners() .setFragmentResultListener(QRCODE_SCAN, this, (requestKey, b) -> { ScanOptions options = Utils.getQRScanOptions(this); + hideDialog(); qrCodeScanner.launch(options); }); } - //TODO: Implement all QR scan using thie method + //TODO: Implement all QR scan using this method private final ActivityResultLauncher qrCodeScanner = registerForActivityResult(new ScanContract(), result -> { if(result.getContents() == null) @@ -1129,9 +1127,13 @@ private void checkIntents(String importData, String importPath, Intent startInte { try { - if (importData != null) importData = URLDecoder.decode(importData, "UTF-8"); - DappBrowserFragment dappFrag = (DappBrowserFragment) getFragment(DAPP_BROWSER); - if (importData != null && importData.startsWith(NotificationService.AWSTARTUP)) + if (importData != null) importData = Utils.universalURLDecode(importData); + + if (importData != null && viewModel.handleSmartPass(this, importData)) + { + //Complete + } + else if (importData != null && importData.startsWith(NotificationService.AWSTARTUP)) { importData = importData.substring(NotificationService.AWSTARTUP.length()); //move window to token if found @@ -1139,6 +1141,7 @@ private void checkIntents(String importData, String importPath, Intent startInte } else if (startIntent.getStringExtra("url") != null) { + DappBrowserFragment dappFrag = (DappBrowserFragment) getFragment(DAPP_BROWSER); String url = startIntent.getStringExtra("url"); showPage(DAPP_BROWSER); if (!dappFrag.isDetached()) dappFrag.loadDirect(url); @@ -1319,23 +1322,98 @@ public int getItemCount() } } - public void importAttestation(QRResult attestation) + @Override + public void attestationImported(TokenCardMeta newToken) { - if (attestation.type != EIP681Type.ATTESTATION) - { - return; - } + runOnUiThread(() -> { + BaseFragment frag = getFragment(WALLET); + if (frag instanceof WalletFragment) + { + ((WalletFragment) frag).updateAttestationMeta(newToken); //add to wallet + } + + //display import success + if (dialog == null || !dialog.isShowing()) + { + AWalletAlertDialog aDialog = new AWalletAlertDialog(this); + + aDialog.setTitle(R.string.attestation_imported); + aDialog.setIcon(AWalletAlertDialog.SUCCESS); + aDialog.setButtonText(R.string.button_ok); + aDialog.setButtonListener(v -> + aDialog.dismiss()); + dialog = aDialog; + dialog.show(); + } + }); + } + + @Override + public void importError(String error) + { + runOnUiThread(() -> { + hideDialog(); + AWalletAlertDialog aDialog = new AWalletAlertDialog(this); - ((WalletFragment)getFragment(WALLET)).importAttestation(attestation); + aDialog.setTitle(R.string.attestation_import_failed); + aDialog.setMessage(error); + aDialog.setIcon(AWalletAlertDialog.ERROR); + aDialog.setButtonText(R.string.button_ok); + aDialog.setButtonListener(v -> + aDialog.dismiss()); + dialog = aDialog; + dialog.show(); + }); } - public void importEASAttestation(QRResult attestation) + @Override + public void smartPassValidation(SmartPassReturn validation) { - if (attestation.type != EIP681Type.EAS_ATTESTATION) + //Handle smart pass return + switch (validation) { - return; + case ALREADY_IMPORTED: + //No need to report anything + break; + case IMPORT_SUCCESS: + importedSmartPass(); + break; + case IMPORT_FAILED: + //No need to report anything? + break; + case NO_CONNECTION: + showNoConnection(); + break; } + } + + private void showNoConnection() + { + runOnUiThread(() -> { + AWalletAlertDialog aDialog = new AWalletAlertDialog(this); + aDialog.setTitle(R.string.no_connection); + aDialog.setMessage(R.string.no_connection_to_smart_layer); + aDialog.setIcon(AWalletAlertDialog.WARNING); + aDialog.setButtonText(R.string.button_ok); + aDialog.setButtonListener(v -> + aDialog.dismiss()); + dialog = aDialog; + dialog.show(); + }); + } - ((WalletFragment)getFragment(WALLET)).importEASAttestation(attestation); + private void importedSmartPass() + { + runOnUiThread(() -> { + AWalletAlertDialog aDialog = new AWalletAlertDialog(this); + aDialog.setTitle(R.string.imported_smart_pass); + aDialog.setMessage(R.string.smartpass_imported); + aDialog.setIcon(AWalletAlertDialog.SUCCESS); + aDialog.setButtonText(R.string.button_ok); + aDialog.setButtonListener(v -> + aDialog.dismiss()); + dialog = aDialog; + dialog.show(); + }); } } diff --git a/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java b/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java index 74290b4b95..ad637b20a5 100644 --- a/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/ImportWalletActivity.java @@ -572,7 +572,7 @@ private String extractAddressFromStore(String store) try { JSONObject jsonObject = new JSONObject(store); - return "0x" + Numeric.cleanHexPrefix(jsonObject.getString("address")); + return Numeric.prependHexPrefix(jsonObject.getString("address")); } catch (JSONException ex) { diff --git a/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java b/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java index d4001b1c47..0ed3949250 100644 --- a/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/MyAddressActivity.java @@ -201,7 +201,7 @@ private void showPointOfSaleMode() findViewById(R.id.toolbar_title).setVisibility(View.GONE); setTitle(""); displayAddress = Keys.toChecksumAddress(wallet.address); - networkInfo = viewModel.getEthereumNetworkRepository().getNetworkByChain(overrideNetwork); + networkInfo = viewModel.getNetworkByChain(overrideNetwork); currentMode = AddressMode.MODE_POS; layoutInputAmount.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java index 2ef2a37f91..2d823e5a73 100644 --- a/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java +++ b/app/src/main/java/com/alphawallet/app/ui/NFTAssetDetailActivity.java @@ -29,6 +29,7 @@ import com.alphawallet.app.BuildConfig; import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.GasEstimate; import com.alphawallet.app.entity.SignAuthenticationCallback; import com.alphawallet.app.entity.StandardFunctionInterface; @@ -37,6 +38,7 @@ import com.alphawallet.app.entity.WalletType; import com.alphawallet.app.entity.nftassets.NFTAsset; import com.alphawallet.app.entity.opensea.OpenSeaAsset; +import com.alphawallet.app.entity.tokens.Attestation; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.service.GasService; import com.alphawallet.app.ui.widget.entity.ActionSheetCallback; @@ -115,6 +117,8 @@ public class NFTAssetDetailActivity extends BaseActivity implements StandardFunc private long chainId; private Disposable disposable; + private boolean loadingInProgress; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -159,10 +163,13 @@ public void onResume() super.onResume(); if (viewModel != null) { - progressBar.setVisibility(View.VISIBLE); - viewModel.prepare(); - getIntentData(); - tokenImage.onResume(); + if (!loadingInProgress) + { + progressBar.setVisibility(View.VISIBLE); + viewModel.prepare(); + getIntentData(); + tokenImage.onResume(); + } } else { @@ -187,6 +194,7 @@ public void onPause() { super.onPause(); tokenImage.onPause(); + loadingInProgress = false; } @Override @@ -307,6 +315,7 @@ private void showWarnDialog(String walletAddress) private void setup() { + loadingInProgress = true; viewModel.checkForNewScript(token); viewModel.checkTokenScriptValidity(token); setTitle(token.tokenInfo.name); @@ -493,6 +502,7 @@ private void loadAssetFromMetadata(NFTAsset loadedAsset) loadFromOpenSeaData(loadedAsset.getOpenSeaAsset()); completeTokenScriptSetup(); + loadingInProgress = false; } } @@ -620,7 +630,11 @@ private void setupAttestation() { NFTAsset attnAsset = new NFTAsset(); TokenDefinition td = viewModel.getAssetDefinitionService().getAssetDefinition(token); - if (td != null) + if (token.getInterfaceSpec() != ContractType.ATTESTATION) + { + return; + } + else if (td != null) { attnAsset.setupScriptElements(td); attnAsset.setupScriptAttributes(td, token); @@ -634,13 +648,14 @@ private void setupAttestation() } else { - tokenImage.setImageResource(R.drawable.zero_one_block); + tokenImage.setAttestationImage(token); token.addAssetElements(attnAsset, this); tokenDescription.setVisibility(View.GONE); } progressBar.setVisibility(View.GONE); tivTokenId.setVisibility(View.GONE); + showIssuer(((Attestation)token).getIssuer()); //now populate nftAttributeLayout.bind(token, attnAsset); @@ -777,6 +792,15 @@ private void checkConfirm(Web3Transaction w3tx) confirmationDialog.show(); } + private void showIssuer(String issuer) + { + if (!TextUtils.isEmpty(issuer)) + { + ((TokenInfoView)findViewById(R.id.key_address)).setCopyableValue(issuer); + ((TokenInfoView)findViewById(R.id.key_address)).setVisibility(View.VISIBLE); + } + } + @Override public void getAuthorisation(SignAuthenticationCallback callback) { diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java b/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java index c1572805dd..0b05b01849 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletConnectV2Activity.java @@ -180,6 +180,11 @@ private void onDefaultWallet(Wallet wallet) private void displaySessionStatus(WalletConnectV2SessionItem session, Wallet wallet) { + if (session == null) + { + return; + } + if (session.icon == null) { icon.setImageResource(R.drawable.grey_circle); diff --git a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java index a3f911b45d..31fc96e505 100644 --- a/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java +++ b/app/src/main/java/com/alphawallet/app/ui/WalletFragment.java @@ -42,12 +42,10 @@ import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.CustomViewSettings; import com.alphawallet.app.entity.ErrorEnvelope; -import com.alphawallet.app.entity.QRResult; import com.alphawallet.app.entity.ServiceSyncCallback; import com.alphawallet.app.entity.TokenFilter; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletType; -import com.alphawallet.app.entity.tokendata.TokenGroup; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.entity.tokens.TokenCardMeta; import com.alphawallet.app.interact.GenericWalletInteract; @@ -243,7 +241,6 @@ private void initViewModel() viewModel.defaultWallet().observe(getViewLifecycleOwner(), this::onDefaultWallet); viewModel.onFiatValues().observe(getViewLifecycleOwner(), this::updateValue); viewModel.onUpdatedTokens().observe(getViewLifecycleOwner(), this::updateMetas); - viewModel.attestationError().observe(getViewLifecycleOwner(), this::attestationError); viewModel.getTokensService().startWalletSync(this); viewModel.activeWalletConnectSessions().observe(getViewLifecycleOwner(), walletConnectSessionItems -> { adapter.showActiveWalletConnectSessions(walletConnectSessionItems); @@ -300,16 +297,12 @@ private void updateMetas(TokenCardMeta[] metas) systemView.hide(); viewModel.checkDeleteMetas(metas); viewModel.calculateFiatValues(); - checkAttestationNotice(metas); } } - private void checkAttestationNotice(TokenCardMeta[] metas) + public void updateAttestationMeta(TokenCardMeta tcm) { - if (metas.length == 1 && metas[0].group == TokenGroup.ATTESTATION) - { - Toast.makeText(getActivity(), "Attestation Imported", Toast.LENGTH_SHORT).show(); - } + updateMetas(new TokenCardMeta[]{tcm}); } //Refresh value of wallet once sync is complete @@ -374,6 +367,7 @@ public void comeIntoFocus() { isVisible = true; viewModel.startUpdateListener(); + viewModel.getTokensService().startUpdateCycle(); } @Override @@ -776,35 +770,6 @@ public void onSwitchClicked() networkSettingsHandler.launch(intent); } - public void importAttestation(QRResult attestation) - { - viewModel.importAttestation(attestation); - } - - public void importEASAttestation(QRResult attestation) - { - viewModel.importEASAttestation(attestation); - } - - private void attestationError(String message) - { - if (dialog == null) - { - dialog = new AWalletAlertDialog(requireContext()); - } - else - { - dialog.dismiss(); - } - - dialog.setIcon(AWalletAlertDialog.ERROR); - dialog.setTitle(R.string.attestation); - dialog.setMessage(message); - dialog.setButtonText(R.string.dialog_ok); - dialog.setButtonListener(v -> dialog.dismiss()); - dialog.show(); - } - public class SwipeCallback extends ItemTouchHelper.SimpleCallback { private final TokensAdapter mAdapter; diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java index c78220dfd9..a6b1bc2783 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/adapter/ChainAdapter.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import com.alphawallet.app.R; +import com.alphawallet.app.entity.NetworkInfo; import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.walletconnect.util.WalletConnectHelper; @@ -44,8 +45,18 @@ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup TextView chainName = convertView.findViewById(R.id.chain_name); ImageView chainIcon = convertView.findViewById(R.id.chain_icon); - chainName.setText(EthereumNetworkBase.getNetworkInfo(chainId).name); - chainIcon.setImageResource(EthereumNetworkRepository.getChainLogo(chainId)); + NetworkInfo info = EthereumNetworkBase.getNetworkInfo(chainId); + + if (info != null) + { + chainName.setText(EthereumNetworkBase.getNetworkInfo(chainId).name); + chainIcon.setImageResource(EthereumNetworkRepository.getChainLogo(chainId)); + } + else + { + chainName.setText("Unhandled Chain"); + chainIcon.setImageResource(EthereumNetworkRepository.getChainLogo(R.drawable.ic_goerli)); + } return convertView; } diff --git a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java index 9acb3fa7c4..04d4584e2c 100644 --- a/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java +++ b/app/src/main/java/com/alphawallet/app/ui/widget/holder/TokenHolder.java @@ -39,7 +39,6 @@ import com.google.android.material.checkbox.MaterialCheckBox; import java.math.BigDecimal; -import java.math.BigInteger; import java.math.RoundingMode; import java.util.Locale; @@ -195,7 +194,14 @@ private void handleAttestation(TokenCardMeta data) balanceEth.setText(attestation.getAttestationName(td)); balanceCoin.setText(attestation.getAttestationDescription(td)); balanceCoin.setVisibility(View.VISIBLE); - tokenIcon.setAttestationIcon(nftAsset.getImage(), attestation.getSymbol(), data.getChain()); + if (attestation.isSmartPass()) + { + tokenIcon.setSmartPassIcon(data.getChain()); + } + else + { + tokenIcon.setAttestationIcon(nftAsset.getImage(), attestation.getSymbol(), data.getChain()); + } token = attestation; blankTickerInfo(); } diff --git a/app/src/main/java/com/alphawallet/app/util/QRParser.java b/app/src/main/java/com/alphawallet/app/util/QRParser.java index c7f9ef12df..db1128e118 100644 --- a/app/src/main/java/com/alphawallet/app/util/QRParser.java +++ b/app/src/main/java/com/alphawallet/app/util/QRParser.java @@ -89,17 +89,17 @@ public QRResult parse(String url) if (url == null) return null; - if (Utils.hasAttestation(url)) + if (Utils.hasEASAttestation(url)) { result = new QRResult(url); result.type = EIP681Type.EAS_ATTESTATION; - result.functionDetail = Utils.decompress(url); + String taglessAttestation = Utils.parseEASAttestation(url); + result.functionDetail = Utils.toAttestationJson(taglessAttestation); return result; } String[] parts = url.split(":"); - if (url.startsWith("wc:")) { return new QRResult(url, EIP681Type.WALLET_CONNECT); 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 8674249f44..2e6aad6e40 100644 --- a/app/src/main/java/com/alphawallet/app/util/Utils.java +++ b/app/src/main/java/com/alphawallet/app/util/Utils.java @@ -15,6 +15,7 @@ import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Typeface; +import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.text.format.DateUtils; @@ -32,7 +33,6 @@ import com.alphawallet.app.R; import com.alphawallet.app.entity.EasAttestation; import com.alphawallet.app.entity.tokens.Token; -import com.alphawallet.app.entity.tokens.TokenInfo; import com.alphawallet.app.util.pattern.Patterns; import com.alphawallet.app.web3j.StructuredDataEncoder; import com.alphawallet.token.entity.ProviderTypedData; @@ -87,6 +87,8 @@ public class Utils private static final String TRUST_ICON_REPO_BASE = "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/"; private static final String TRUST_ICON_REPO = TRUST_ICON_REPO_BASE + CHAIN_REPO_ADDRESS_TOKEN + "/assets/" + ICON_REPO_ADDRESS_TOKEN + TOKEN_LOGO; private static final String ALPHAWALLET_ICON_REPO = ALPHAWALLET_REPO_NAME + ICON_REPO_ADDRESS_TOKEN + TOKEN_LOGO; + private static final String ATTESTATION_PREFIX = "#attestation="; + private static final String SMART_PASS_PREFIX = "ticket="; public static int dp2px(Context context, int dp) { @@ -1126,55 +1128,97 @@ public static boolean isAlphaWallet(Context context) return context.getPackageName().equals("io.stormbird.wallet"); } - /*public static boolean hasAttestation(String url) + //Decode heuristic: + //1. use Android URI parser to extract "ticket" or "attestation" + //2. do URL decode on the extracted string + //3. Base64 decode and unzip, return decoded string if any + //4. Try the decode step "_ -> /" and "- -> +" and try Base64 decode and unzip, return decoded string + //5. Try EAS format: extract tag from "ticket=" or "#attestation=" and try Base64 decode and unzip. + public static String parseEASAttestation(String data) { - result.functionDetail = Utils.decompress(url); - - int hashIndex = url.indexOf("#attestation="); - if (hashIndex >= 0) + String inflate; + String attestation = attestationViaParams(data); + if (!TextUtils.isEmpty(attestation)) { - url = url.substring(hashIndex + 13); - return url.length() > 10; + //try decode without conversion + inflate = inflateData(attestation); + if (!TextUtils.isEmpty(inflate)) + { + return inflate; + } + String decoded = attestation.replace("_", "/").replace("-", "+"); + inflate = inflateData(decoded); + if (!TextUtils.isEmpty(inflate)) + { + return inflate; + } } - else + + //now check via pulling params directly + attestation = extractParam(data, SMART_PASS_PREFIX); + inflate = inflateData(attestation); + if (!TextUtils.isEmpty(inflate)) { - //detect a base64 attestation - try - { - byte[] tryBase64Data = Base64.decode(url, Base64.DEFAULT); //is this a base64 string? + return inflate; + } + attestation = extractParam(data, ATTESTATION_PREFIX); + return inflateData(attestation); + } - if (tryBase64Data.length > 0 ) - { - return true; - } + public static boolean hasEASAttestation(String data) + { + return parseEASAttestation(data).length() > 0; + } + + //Used to pull the raw attestation zip from the magiclink + public static String extractRawAttestation(String data) + { + String inflate; + String attestation = attestationViaParams(data); + if (!TextUtils.isEmpty(attestation)) + { + //try decode without conversion + inflate = inflateData(attestation); + if (!TextUtils.isEmpty(inflate)) + { + return attestation; } - catch (IllegalArgumentException e) + String decoded = attestation.replace("_", "/").replace("-", "+"); + inflate = inflateData(decoded); + if (!TextUtils.isEmpty(inflate)) { - // no action, return false; - Timber.w(e); + return attestation; } + } - return false; + //now check via pulling params directly + attestation = extractParam(data, SMART_PASS_PREFIX); + inflate = inflateData(attestation); + if (!TextUtils.isEmpty(inflate)) + { + return attestation; } - }*/ - public static String getAttestationString(String url) + attestation = extractParam(data, ATTESTATION_PREFIX); + return inflateData(attestation); + } + + private static String extractParam(String url, String param) { - int hashIndex = url.indexOf("#attestation="); + int paramIndex = url.indexOf(param); String decoded; try { - if (hashIndex >= 0) //EAS style attestations have the magic link style + if (paramIndex >= 0) //EAS style attestations have the magic link style { - url = url.substring(hashIndex + 13); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + url = url.substring(paramIndex + param.length()); + decoded = universalURLDecode(url); + //find end param if there is one + int endIndex = decoded.indexOf("&"); + if (endIndex > 0) { - decoded = URLDecoder.decode(url, StandardCharsets.UTF_8); - } - else - { - decoded = URLDecoder.decode(url, "UTF-8"); + decoded = decoded.substring(0, endIndex); } } else @@ -1191,39 +1235,64 @@ public static String getAttestationString(String url) return decoded; } - public static TokenInfo getDefaultAttestationInfo(long chainId, String collectionHash) - { - return new TokenInfo(collectionHash, "EAS Attestation", "ATTN", 0, true, chainId); - } - - public static boolean hasAttestation(String data) + private static String attestationViaParams(String url) { + String decoded = ""; try { - String inflated = inflateData(getAttestationString(data)); - return inflated.length() > 0; + Uri uri = Uri.parse(url); + String payload = uri.getQueryParameter("ticket"); + if (TextUtils.isEmpty(payload)) + { + payload = uri.getQueryParameter("attestation"); + } + + if (TextUtils.isEmpty(payload)) + { + return ""; + } + + decoded = universalURLDecode(payload); + //decoded = decoded.replace("_", "/").replace("-", "+"); + + Timber.d("decoded url: %s", decoded); } catch (Exception e) { - return false; + // Expected } + return decoded; } - public static String decompress(String url) + public static String universalURLDecode(String url) { + String decoded; try { - //Timber.d(toAttestationJson(inflateData(getAttestationString(url)))); - return toAttestationJson(inflateData(getAttestationString(url))); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + { + decoded = URLDecoder.decode(url, StandardCharsets.UTF_8); + } + else + { + decoded = URLDecoder.decode(url, "UTF-8"); + } } catch (Exception e) { - return null; + decoded = url; } + + return decoded; } - private static String toAttestationJson(String jsonString) + public static String toAttestationJson(String jsonString) { + if (TextUtils.isEmpty(jsonString)) + { + return ""; + } + // Remove the square brackets jsonString = jsonString.substring(1, jsonString.length() - 1); String[] e = jsonString.split(","); @@ -1234,6 +1303,18 @@ private static String toAttestationJson(String jsonString) e[i] = e[i].replaceAll("\"", ""); } + long versionParam = 0; + + if (e.length < 16 || e.length > 17) + { + return ""; + } + + if (e.length == 17) + { + versionParam = Long.parseLong(e[16]); + } + EasAttestation easAttestation = new EasAttestation( e[0], @@ -1251,7 +1332,8 @@ private static String toAttestationJson(String jsonString) e[12], Boolean.parseBoolean(e[13]), e[14], - Long.parseLong(e[15]) + Long.parseLong(e[15]), + versionParam ); return new Gson().toJson(easAttestation); @@ -1259,15 +1341,15 @@ private static String toAttestationJson(String jsonString) public static String inflateData(String deflatedData) { - byte[] deflatedBytes = Base64.decode(deflatedData, Base64.DEFAULT); + try + { + byte[] deflatedBytes = Base64.decode(deflatedData, Base64.DEFAULT); - Inflater inflater = new Inflater(); - inflater.setInput(deflatedBytes); + Inflater inflater = new Inflater(); + inflater.setInput(deflatedBytes); - byte[] inflatedData; + byte[] inflatedData; - try - { // Inflate the data ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java index eec78d7c4e..8f3acd56f5 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/CustomNetworkViewModel.java @@ -39,7 +39,7 @@ public void saveNetwork(boolean isEditMode, String name, String rpcUrl, long cha public NetworkInfo getNetworkInfo(long chainId) { - return this.ethereumNetworkRepository.getNetworkByChain(chainId); + return ethereumNetworkRepository.getNetworkByChain(chainId); } public boolean isTestNetwork(NetworkInfo network) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java index 52e40d4662..6eb350adf9 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/HomeViewModel.java @@ -30,6 +30,7 @@ import com.alphawallet.app.analytics.Analytics; import com.alphawallet.app.entity.AnalyticsProperties; import com.alphawallet.app.entity.CryptoFunctions; +import com.alphawallet.app.entity.EIP681Type; import com.alphawallet.app.entity.FragmentMessenger; import com.alphawallet.app.entity.GitHubRelease; import com.alphawallet.app.entity.NetworkInfo; @@ -39,6 +40,7 @@ import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.WalletConnectActions; import com.alphawallet.app.entity.analytics.QrScanResultType; +import com.alphawallet.app.entity.attestation.AttestationImport; import com.alphawallet.app.interact.FetchWalletsInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.repository.CurrencyRepositoryType; @@ -133,6 +135,7 @@ public class HomeViewModel extends BaseViewModel private final ExternalBrowserRouter externalBrowserRouter; private final OkHttpClient httpClient; private final RealmManager realmManager; + private final TokensService tokensService; private final AlphaWalletNotificationService alphaWalletNotificationService; private final MutableLiveData walletName = new MutableLiveData<>(); private final MutableLiveData defaultWallet = new MutableLiveData<>(); @@ -158,6 +161,7 @@ public class HomeViewModel extends BaseViewModel ExternalBrowserRouter externalBrowserRouter, OkHttpClient httpClient, RealmManager realmManager, + TokensService tokensService, AlphaWalletNotificationService alphaWalletNotificationService) { this.preferenceRepository = preferenceRepository; @@ -176,6 +180,7 @@ public class HomeViewModel extends BaseViewModel this.alphaWalletNotificationService = alphaWalletNotificationService; setAnalyticsService(analyticsService); this.preferenceRepository.incrementLaunchCount(); + this.tokensService = tokensService; } @Override @@ -367,7 +372,7 @@ public void setErrorCallback(FragmentMessenger callback) assetDefinitionService.setErrorCallback(callback); } - public void handleQRCode(Activity activity, String qrCode) + public void handleQRCode(HomeActivity activity, String qrCode) { try { @@ -378,8 +383,9 @@ public void handleQRCode(Activity activity, String qrCode) QRResult qrResult = parser.parse(qrCode); switch (qrResult.type) { + case ATTESTATION: case EAS_ATTESTATION: - ((HomeActivity) activity).importEASAttestation(qrResult); + handleImportAttestation(activity, qrResult); break; case ADDRESS: props.put(QrScanResultType.KEY, QrScanResultType.ADDRESS.getValue()); @@ -406,8 +412,7 @@ public void handleQRCode(Activity activity, String qrCode) case URL: props.put(QrScanResultType.KEY, QrScanResultType.URL.getValue()); track(Analytics.Action.SCAN_QR_CODE_SUCCESS, props); - - ((HomeActivity) activity).onBrowserWithURL(qrCode); + activity.onBrowserWithURL(qrCode); break; case MAGIC_LINK: showImportLink(activity, qrCode); @@ -417,9 +422,6 @@ public void handleQRCode(Activity activity, String qrCode) break; case OTHER_PROTOCOL: break; - case ATTESTATION: - ((HomeActivity) activity).importAttestation(qrResult); - break; case WALLET_CONNECT: startWalletConnect(activity, qrCode); break; @@ -437,7 +439,6 @@ public void handleQRCode(Activity activity, String qrCode) Toast.makeText(activity, R.string.toast_invalid_code, Toast.LENGTH_SHORT).show(); } } - private void startWalletConnect(Activity activity, String qrCode) { Intent intent; @@ -682,7 +683,6 @@ private Request getRequest() private TokenDefinition parseFile(Context ctx, InputStream xmlInputStream) throws Exception { Locale locale = ctx.getResources().getConfiguration().getLocales().get(0); - //AttestationParser ap = new AttestationParser(xmlInputStream, locale, null); return new TokenDefinition( xmlInputStream, locale, null); } @@ -906,4 +906,60 @@ public boolean isWatchOnlyWallet() { return preferenceRepository.isWatchOnly(); } + + private Single getCurrentWallet() + { + if (defaultWallet.getValue() != null) + { + return Single.fromCallable(defaultWallet::getValue); + } + else + { + return genericWalletInteract.find(); + } + } + + private void handleImportAttestation(HomeActivity activity, QRResult qrResult) + { + disposable = getCurrentWallet() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(wallet -> completeImport(activity, wallet, qrResult)); + } + + private void completeImport(HomeActivity activity, Wallet wallet, QRResult qrResult) + { + if (wallet == null || wallet.watchOnly()) + { + activity.importError(activity.getString(R.string.watch_wallet)); + } + else + { + AttestationImport attnImport = new AttestationImport(assetDefinitionService, tokensService, + activity, wallet, realmManager, httpClient); + + //attempt to import the wallet + attnImport.importAttestation(qrResult); + } + } + + public boolean handleSmartPass(HomeActivity homeActivity, String smartPassCandidate) + { + if (smartPassCandidate.startsWith(AttestationImport.SMART_PASS_URL)) + { + smartPassCandidate = smartPassCandidate.substring(AttestationImport.SMART_PASS_URL.length()); //chop off leading URL + QRResult result = new QRResult(smartPassCandidate); + result.type = EIP681Type.EAS_ATTESTATION; + String taglessAttestation = Utils.parseEASAttestation(smartPassCandidate); + result.functionDetail = Utils.toAttestationJson(taglessAttestation); + + if (!TextUtils.isEmpty(result.functionDetail)) + { + handleImportAttestation(homeActivity, result); + return true; + } + } + + return false; + } } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/MyAddressViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/MyAddressViewModel.java index 7cc3298a44..5f303b90f1 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/MyAddressViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/MyAddressViewModel.java @@ -29,10 +29,6 @@ public TokensService getTokenService() { return tokenService; } - public EthereumNetworkRepositoryType getEthereumNetworkRepository() { - return ethereumNetworkRepository; - } - public NetworkInfo getNetworkByChain(long chainId) { return ethereumNetworkRepository.getNetworkByChain(chainId); } diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NetworkChooserViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NetworkChooserViewModel.java index 13879ad7fd..caef506df1 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NetworkChooserViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NetworkChooserViewModel.java @@ -4,7 +4,6 @@ import android.content.Intent; import com.alphawallet.app.entity.NetworkInfo; -import com.alphawallet.app.repository.EthereumNetworkBase; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.repository.PreferenceRepositoryType; import com.alphawallet.app.service.TokensService; @@ -21,16 +20,13 @@ public class NetworkChooserViewModel extends BaseViewModel { private final EthereumNetworkRepositoryType networkRepository; private final TokensService tokensService; - private final PreferenceRepositoryType preferenceRepository; @Inject public NetworkChooserViewModel(EthereumNetworkRepositoryType ethereumNetworkRepositoryType, - TokensService tokensService, - PreferenceRepositoryType preferenceRepository) + TokensService tokensService) { this.networkRepository = ethereumNetworkRepositoryType; this.tokensService = tokensService; - this.preferenceRepository = preferenceRepository; } public NetworkInfo[] getNetworkList() @@ -54,11 +50,6 @@ public NetworkInfo getNetworkByChain(long chainId) return networkRepository.getNetworkByChain(chainId); } - public boolean isMainNet(long networkId) - { - return EthereumNetworkBase.hasRealValue(networkId); - } - public long getSelectedNetwork() { NetworkInfo browserNetwork = networkRepository.getActiveBrowserNetwork(); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/NetworkToggleViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/NetworkToggleViewModel.java index 89dc6eca60..61ad6e8e31 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/NetworkToggleViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/NetworkToggleViewModel.java @@ -75,11 +75,6 @@ public NetworkInfo getNetworkByChain(long chainId) return networkRepository.getNetworkByChain(chainId); } - public boolean testnetEnabled() - { - return preferenceRepository.isTestnetEnabled(); - } - public List getNetworkList(boolean isMainNet) { List networkList = new ArrayList<>(); diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java index 71f00f7681..0568548c1d 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/TokenFunctionViewModel.java @@ -215,16 +215,6 @@ public MutableLiveData> gasEstimateError() return gasEstimateError; } - public MutableLiveData> traits() - { - return traits; - } - - public MutableLiveData assetContract() - { - return assetContract; - } - public MutableLiveData nftAsset() { return nftAsset; diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java index a745f0d9de..34548df189 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletConnectViewModel.java @@ -30,6 +30,7 @@ import com.alphawallet.app.interact.FindDefaultNetworkInteract; import com.alphawallet.app.interact.GenericWalletInteract; import com.alphawallet.app.interact.WalletConnectInteract; +import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.app.repository.EthereumNetworkRepositoryType; import com.alphawallet.app.repository.SignRecord; import com.alphawallet.app.repository.entity.RealmWCSession; diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java index 4ab2869eeb..4425e93328 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletViewModel.java @@ -17,7 +17,6 @@ import android.view.View; import android.widget.Toast; -import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -65,8 +64,6 @@ import com.alphawallet.token.tools.TokenDefinition; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; -import com.journeyapps.barcodescanner.ScanContract; -import com.journeyapps.barcodescanner.ScanOptions; import com.google.gson.Gson; import org.jetbrains.annotations.NotNull; @@ -100,8 +97,6 @@ public class WalletViewModel extends BaseViewModel private final MutableLiveData defaultWallet = new MutableLiveData<>(); private final MutableLiveData backupEvent = new MutableLiveData<>(); private final MutableLiveData> fiatValues = new MutableLiveData<>(); - private final MutableLiveData attestationError = new MutableLiveData<>(); - private final FetchTokensInteract fetchTokensInteract; private final TokensMappingRepositoryType tokensMappingRepository; private final TokenDetailRouter tokenDetailRouter; @@ -182,11 +177,6 @@ public LiveData> onFiatValues() return fiatValues; } - public LiveData attestationError() - { - return attestationError; - } - public String getWalletAddr() { return defaultWallet.getValue() != null ? defaultWallet.getValue().address : ""; @@ -575,203 +565,6 @@ private Single getUpdatedTokenMetas() }); } - public void importAttestation(QRResult attestation) - { - //Get token information - assume attestation is based on NFT - //TODO: First validate Attestation - tokensService.update(attestation.getAddress(), attestation.chainId, ContractType.ERC721) - .flatMap(tInfo -> getTokensService().storeTokenInfoDirect(getWallet(), tInfo, ContractType.ERC721)) - .flatMap(tInfo -> storeAttestation(attestation, tInfo)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(attn -> completeImport(attestation, attn), this::onError) - .isDisposed(); - } - - private void completeImport(QRResult attestation, Attestation tokenAttn) - { - if (tokenAttn.isValid() == AttestationValidationStatus.Pass) - { - TokenCardMeta tcmAttestation = new TokenCardMeta(attestation.chainId, attestation.getAddress(), "1", System.currentTimeMillis(), - assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); - tcmAttestation.isEnabled = true; - updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); - } - } - - @SuppressWarnings("checkstyle:MissingSwitchDefault") - private Single storeAttestation(QRResult attestation, TokenInfo tInfo) - { - Attestation attn = assetDefinitionService.validateAttestation(attestation.getAttestation(), tInfo); - switch (attn.isValid()) - { - case Pass: - return storeAttestationInternal(attestation, tInfo, attn); - case Expired: - case Issuer_Not_Valid: - case Incorrect_Subject: - attestationError.postValue(attn.isValid().getValue()); - break; - } - - return Single.fromCallable(() -> attn); - } - - private Single storeAttestationInternal(QRResult attestation, TokenInfo tInfo, Attestation attn) - { - //complete the import - //write to realm - return Single.fromCallable(() -> { - try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue())) - { - realm.executeTransaction(r -> { - -// RealmResults realmAssets = realm.where(RealmAttestation.class) -// .findAll(); -// -// realmAssets.deleteAllFromRealm(); - - String key = attn.getDatabaseKey(); - RealmAttestation realmAttn = r.where(RealmAttestation.class) - .equalTo("address", key) - .findFirst(); - - if (realmAttn == null) - { - realmAttn = r.createObject(RealmAttestation.class, key); - } - - attn.populateRealmAttestation(realmAttn); - }); - } - catch (Exception e) - { - e.printStackTrace(); - } - return attn; - }).flatMap(generatedAttestation -> { - if (tokensService.getToken(tInfo.chainId, tInfo.address) == null) - { - return tokensService.storeTokenInfo(defaultWallet.getValue(), tInfo, ContractType.ERC721); - } - else - { - return Single.fromCallable(() -> tInfo); - } - }).map(info -> setBaseType(attn, info)); - } - - private Attestation setBaseType(Attestation attn, TokenInfo info) - { - Token baseToken = tokensService.getToken(info.chainId, info.address); - if (baseToken != null) - { - attn.setBaseTokenType(baseToken.getInterfaceSpec()); - } - - return attn; - } - - public void importEASAttestation(QRResult qrAttn) - { - //validate attestation - //get chain and address - EasAttestation easAttn = new Gson().fromJson(qrAttn.functionDetail, EasAttestation.class); - - //validation UID: - storeAttestation(easAttn, qrAttn.functionDetail) - .map(this::completeImport) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::checkTokenScript, this::onError) - .isDisposed(); - } - - @SuppressWarnings("checkstyle:MissingSwitchDefault") - private Single storeAttestation(EasAttestation attestation, String importedAttestation) - { - //Use Default key unless specified - return Single.fromCallable(() -> { - Attestation attn = assetDefinitionService.validateAttestation(attestation, importedAttestation); - switch (attn.isValid()) - { - case Pass: - return storeAttestationInternal(attestation, attn, importedAttestation); - case Expired: - case Issuer_Not_Valid: - case Incorrect_Subject: - attestationError.postValue(attn.isValid().getValue()); - break; - } - - return attn; - }); - } - - private Attestation storeAttestationInternal(EasAttestation attestation, Attestation attn, String importedAttestation) - { - try (Realm realm = realmManager.getRealmInstance(defaultWallet.getValue())) - { - realm.executeTransaction(r -> { - String key = attn.getDatabaseKey(); - RealmAttestation realmAttn = r.where(RealmAttestation.class) - .equalTo("address", key) - .findFirst(); - - if (realmAttn == null) - { - realmAttn = r.createObject(RealmAttestation.class, key); - } - - realmAttn.setId(importedAttestation); - attn.populateRealmAttestation(realmAttn); - }); - } - catch (Exception e) - { - e.printStackTrace(); - } - return attn; - } - - private Token completeImport(Token token) - { - if (token instanceof Attestation && ((Attestation)token).isValid() == AttestationValidationStatus.Pass) - { - Attestation tokenAttn = (Attestation)token; - TokenCardMeta tcmAttestation = new TokenCardMeta(tokenAttn.tokenInfo.chainId, tokenAttn.getAddress(), "1", System.currentTimeMillis(), - assetDefinitionService, tokenAttn.tokenInfo.name, tokenAttn.tokenInfo.symbol, tokenAttn.getBaseTokenType(), - TokenGroup.ATTESTATION, tokenAttn.getAttestationUID()); - tcmAttestation.isEnabled = true; - updatedTokens.postValue(new TokenCardMeta[]{tcmAttestation}); - } - - return token; - } - - public void checkTokenScript(Token token) - { - //check server for a TokenScript - disposable = assetDefinitionService.checkServerForScript(token, null) - .observeOn(Schedulers.io()) - .subscribeOn(Schedulers.single()) - .subscribe(td -> handleFilename(td, token), Timber::w); - } - - private void handleFilename(TokenDefinition td, Token token) - { - switch (td.nameSpace) - { - case UNCHANGED_SCRIPT: - case NO_SCRIPT: - break; - default: - //found a new script - completeImport(token); - break; - } - } - public void removeTokenMetaItem(String tokenKeyId) { final String tokenKey = tokenKeyId.endsWith(Attestation.ATTESTATION_SUFFIX) ? tokenKeyId.substring(0, tokenKeyId.length() - Attestation.ATTESTATION_SUFFIX.length()) diff --git a/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java b/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java index f2ce3e570d..5340a75e52 100644 --- a/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java +++ b/app/src/main/java/com/alphawallet/app/viewmodel/WalletsViewModel.java @@ -45,6 +45,7 @@ import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; import java.math.BigInteger; import java.security.SignatureException; @@ -592,7 +593,7 @@ public void storeHardwareWallet(SignatureFromKey returnSig) throws SignatureExce { Sign.SignatureData sigData = CryptoFunctions.sigFromByteArray(returnSig.signature); BigInteger recoveredKey = Sign.signedMessageToKey(TEST_STRING.getBytes(), sigData); - String address = "0x" + Keys.getAddress(recoveredKey); + String address = Numeric.prependHexPrefix(Keys.getAddress(recoveredKey)); disposable = fetchWalletsInteract .fetch() diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java b/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java index de781c7523..e3f75a0cf4 100644 --- a/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java +++ b/app/src/main/java/com/alphawallet/app/walletconnect/AWWalletConnectClient.java @@ -643,6 +643,11 @@ public void onSessionRequest(@NonNull Model.SessionRequest sessionRequest, @NonN Model.Session settledSession = getSession(sessionRequest.getTopic()); + if (settledSession == null) + { + return; + } + Activity topActivity = App.getInstance().getTopActivity(); if (topActivity != null) { diff --git a/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java b/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java index 7a27334f2a..b164091bb1 100644 --- a/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java +++ b/app/src/main/java/com/alphawallet/app/walletconnect/TransactionDialogBuilder.java @@ -77,6 +77,7 @@ private void initViewModel() viewModel.transactionFinalised().observe(this, this::txWritten); viewModel.transactionSigned().observe(this, this::txSigned); viewModel.transactionError().observe(this, this::txError); + viewModel.startGasCycle(WalletConnectHelper.getChainId(Objects.requireNonNull(sessionRequest.getChainId()))); } @NonNull @@ -226,6 +227,10 @@ public void onCancel(@NonNull DialogInterface dialog) { awWalletConnectClient.reject(sessionRequest); } + if (viewModel != null) + { + viewModel.onDestroy(); + } } @Override @@ -237,5 +242,9 @@ public void onDismiss(@NonNull DialogInterface dialog) { awWalletConnectClient.reject(sessionRequest); } + if (viewModel != null) + { + viewModel.onDestroy(); + } } } diff --git a/app/src/main/java/com/alphawallet/app/web3/entity/Address.java b/app/src/main/java/com/alphawallet/app/web3/entity/Address.java index ce097c9d16..fc4cbe2ecf 100644 --- a/app/src/main/java/com/alphawallet/app/web3/entity/Address.java +++ b/app/src/main/java/com/alphawallet/app/web3/entity/Address.java @@ -7,6 +7,8 @@ import com.alphawallet.app.util.Hex; +import org.web3j.utils.Numeric; + public class Address implements Parcelable { @@ -31,7 +33,7 @@ protected Address(Parcel in) { @Override public String toString() { - return "0x" + value; + return Numeric.prependHexPrefix(value); } @Override diff --git a/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java b/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java index e0c0a7cf17..b94af0b424 100644 --- a/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java +++ b/app/src/main/java/com/alphawallet/app/widget/NFTImageView.java @@ -33,7 +33,9 @@ import com.alphawallet.app.C; import com.alphawallet.app.R; +import com.alphawallet.app.entity.ContractType; import com.alphawallet.app.entity.nftassets.NFTAsset; +import com.alphawallet.app.entity.tokens.Attestation; import com.alphawallet.app.entity.tokens.Token; import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.util.Utils; @@ -514,6 +516,22 @@ public boolean onTouch(View view, MotionEvent motionEvent) return true; } + public void setAttestationImage(Token token) + { + if (token.getInterfaceSpec() == ContractType.ATTESTATION) + { + Attestation attn = (Attestation) token; + if (attn.isSmartPass()) + { + setImageResource(R.drawable.smart_pass); + } + else + { + setImageResource(R.drawable.zero_one_block); + } + } + } + private static class DisplayType { private final ImageType type; diff --git a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java index b805edc5ff..248c62fdc3 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java +++ b/app/src/main/java/com/alphawallet/app/widget/TokenIcon.java @@ -33,13 +33,10 @@ import com.alphawallet.app.ui.widget.TokensAdapterCallback; import com.alphawallet.app.ui.widget.entity.IconItem; import com.alphawallet.app.ui.widget.entity.StatusType; -import com.alphawallet.app.util.RoundedTopCorners; import com.alphawallet.app.util.Utils; import com.bumptech.glide.Glide; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.GlideException; -import com.bumptech.glide.load.resource.bitmap.CenterCrop; -import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.request.Request; import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestOptions; @@ -477,6 +474,16 @@ private void setIsAttestation(String symbol, long chainId) setChainIcon(chainId); } + public void setSmartPassIcon(long chainId) + { + currentRq = Glide.with(this) + .load(R.drawable.smart_pass) + .apply(new RequestOptions().circleCrop()) + .listener(requestListener) + .into(new DrawableImageViewTarget(icon)).getRequest(); + setChainIcon(chainId); + } + public void setAttestationIcon(String image, String symbol, long chain) { if (!TextUtils.isEmpty(image)) diff --git a/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java b/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java index ffa576df28..3b3d062336 100644 --- a/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java +++ b/app/src/main/java/com/alphawallet/app/widget/TransactionDetailWidget.java @@ -18,9 +18,9 @@ import com.alphawallet.app.entity.ActionSheetInterface; import com.alphawallet.app.service.SignatureLookupService; import com.alphawallet.app.web3.entity.Web3Transaction; -import com.alphawallet.token.tools.Numeric; import org.jetbrains.annotations.NotNull; +import org.web3j.utils.Numeric; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -66,7 +66,7 @@ public void setupTransaction(Web3Transaction w3tx, long chainId, String symbol, } else { - String displayText = ("0x" + Numeric.cleanHexPrefix(w3tx.payload)).substring(0, 10); + String displayText = (Numeric.prependHexPrefix(w3tx.payload)).substring(0, 10); textTransactionSummary.setText(displayText); textFunctionName.setText(displayText); } diff --git a/app/src/main/res/drawable/smart_pass.xml b/app/src/main/res/drawable/smart_pass.xml new file mode 100644 index 0000000000..af9395865b --- /dev/null +++ b/app/src/main/res/drawable/smart_pass.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_nft_asset_detail.xml b/app/src/main/res/layout/activity_nft_asset_detail.xml index c3560ddc07..da2be84efd 100644 --- a/app/src/main/res/layout/activity_nft_asset_detail.xml +++ b/app/src/main/res/layout/activity_nft_asset_detail.xml @@ -76,6 +76,13 @@ android:visibility="gone" custom:tokenInfoLabel="@string/contract_address" /> + + Minted Válido desde Válido hasta + Atestación importada + Error al importar la atestación + Dirección del Emisor + Sin Conexión + Smart Pass importado + Se ha importado un pase inteligente. Su pase debe ser actualizado. + No hay conexión a la red Smart Layer: intente importar su Pass nuevamente más tarde. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 189aba2360..e29f1615ea 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -995,4 +995,11 @@ Minted Valable à Compter Valable Jusque + Attestation importée + Échec de l\'importation de l\'attestation + Issuer Address + No Connection + Imported Smart Pass + Smart pass has been imported. Your pass should be upgraded. + No connection to Smart Layer network - try to import your token again later. diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 48da192fc0..d2b19b5842 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -986,4 +986,11 @@ Minted Berlaku dari tanggal Berlaku hingga + Pengesahan Diimpor + Pengesahan Impor Gagal + Alamat Penerbit + Tidak Ada Koneksi + SmartPass yang Diimpor + SmartPass telah diimpor. Pass Anda harus ditingkatkan. + Tidak ada koneksi ke jaringan SmartLayer - coba impor Pass Anda lagi nanti. diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 8a976b3fd2..87c2d57de7 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -1016,4 +1016,11 @@ Minted မှ တရားဝင် သည်အထိ အကျုံးဝင်သည်။ + တင်သွင်းခဲ့သော စုံစမ်းစစ်ဆေးမှု + စမ်းသပ်မှု တင်သွင်းမှု မအောင်မြင်ခဲ့ + ထုတ်ဝေသူ လိပ်စာ + ဆက်သွယ်မှုမရှိပါ + တင်သွင်းတဲ့ SmartPass + SmartPass ကို တင်သွင်းခဲ့ပါတယ်။ ခင်ဗျားရဲ့ လက်မှတ်ကို အဆင့်မြှင့်သင့်ပါတယ်။ + SmartLayer ကွန်ယက်နဲ့ ဆက်သွယ်မှုမရှိပါနဲ့ - နောက်ပိုင်းမှာ SmartPass ကို တင်သွင်းဖို့ ကြိုးစားပါ။ diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index e7523ef664..84ad77f22f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -995,4 +995,11 @@ Minted Có hiệu lực từ ngày Có hiệu lực + Chứng thực được nhập + Nhập chứng thực không thành công + Địa chỉ nhà phát hành + Không có kết nối + SmartPass nhập khẩu + SmartPass đã được nhập. Thẻ của bạn nên được nâng cấp. + Không có kết nối với mạng SmartLayer - hãy thử nhập lại SmartPass của bạn sau. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 1c2bbde986..91b46c6527 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -982,4 +982,11 @@ Minted 有效起始日期 有效期至日期 + 已导入证明 + 证明导入失败 + 发行人地址 + 无连接 + 进口 SmartPass + SmartPass 已导入。您的通票应升级。 + 没有连接到 SmartLayer 网络 - 稍后尝试再次导入您的 SmartPass。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cb0fa1e72..025039e8b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1058,4 +1058,11 @@ Minted Valid From Valid Until + Attestation Imported + Attestation Import Failed + Issuer Address + No Connection + Imported SmartPass + SmartPass has been imported. Your Pass has been upgraded. + No connection to SmartLayer network - try to import your SmartPass again later. diff --git a/app/src/test/java/com/alphawallet/app/QRSelectionTest.java b/app/src/test/java/com/alphawallet/app/QRSelectionTest.java index 67042c9e3d..a7ddfbe564 100644 --- a/app/src/test/java/com/alphawallet/app/QRSelectionTest.java +++ b/app/src/test/java/com/alphawallet/app/QRSelectionTest.java @@ -25,6 +25,7 @@ import org.web3j.crypto.Keys; import org.web3j.crypto.RawTransaction; import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -203,7 +204,7 @@ public Single getMessage(List indexList, String contrac List qrList = new ArrayList<>(); //test key address - String testAddress = "0x" + Keys.getAddress(testKey.getPublicKey()); + String testAddress = Numeric.prependHexPrefix(Keys.getAddress(testKey.getPublicKey())); //generate all ticket redeem combos up to index 256, then check signature and regenerate the selection final int indicesCount = 8 * 2; @@ -263,7 +264,7 @@ public Single getMessage(List indexList, String contrac Sign.SignatureData sigData = sigFromBase64Fix(sPair.signature.signature); //check the signature corresponds to the test address - String addressHex = "0x" + ecRecoverAddress(sPair.message.getBytes(), sigData); + String addressHex = Numeric.prependHexPrefix(ecRecoverAddress(sPair.message.getBytes(), sigData)); // compare BigInteger and Integer. this is quicker than using stream->collect assertEquals(qr.indices.toString(), selectionRecreate.toString()); assertTrue(addressHex.equals(testAddress)); diff --git a/app/src/test/java/com/alphawallet/app/UniversalLinkTest.java b/app/src/test/java/com/alphawallet/app/UniversalLinkTest.java index 18b5668d88..d18ba0abfc 100644 --- a/app/src/test/java/com/alphawallet/app/UniversalLinkTest.java +++ b/app/src/test/java/com/alphawallet/app/UniversalLinkTest.java @@ -140,7 +140,7 @@ public void UniversalLinkShouldBeGeneratedCorrectly() { orders.add(data); } - String ownerAddress = "0x" + ecRecoverAddress(); + String ownerAddress = Numeric.prependHexPrefix(ecRecoverAddress()); //now try to read all the links for (OrderData data : orders) { diff --git a/app/src/test/java/com/alphawallet/app/UniversalLinkTypeTest.java b/app/src/test/java/com/alphawallet/app/UniversalLinkTypeTest.java index 6da623622b..08b0326e26 100644 --- a/app/src/test/java/com/alphawallet/app/UniversalLinkTypeTest.java +++ b/app/src/test/java/com/alphawallet/app/UniversalLinkTypeTest.java @@ -5,6 +5,7 @@ import com.alphawallet.app.repository.EthereumNetworkRepository; import com.alphawallet.token.entity.MagicLinkData; import com.alphawallet.token.entity.SalesOrderMalformed; +import com.alphawallet.token.tools.Numeric; import com.alphawallet.token.tools.ParseMagicLink; import org.web3j.crypto.ECKeyPair; import org.web3j.crypto.Keys; @@ -91,7 +92,7 @@ private void CheckCurrencyLink(String universalLink) assertEquals(data.expiry, expireTomorrow); //check signature - String ownerAddress = "0x" + ecRecoverAddress(); // get testKey address + String ownerAddress = Numeric.prependHexPrefix(ecRecoverAddress()); // get testKey address String recoveredOriginatorAddress = parser.getOwnerKey(data); assertEquals(ownerAddress, recoveredOriginatorAddress); } diff --git a/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockImpl.java b/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockImpl.java index bcbef1e9c2..39cf5acc8b 100644 --- a/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockImpl.java +++ b/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockImpl.java @@ -131,4 +131,16 @@ public String getBlockNativeKey() { return FAKE_KEY_FOR_TESTING; } + + @Override + public String getSmartPassKey() + { + return FAKE_KEY_FOR_TESTING; + } + + @Override + public String getSmartPassDevKey() + { + return FAKE_KEY_FOR_TESTING; + } } diff --git a/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockNonProductionImpl.java b/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockNonProductionImpl.java index ac7db45724..5945698bca 100644 --- a/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockNonProductionImpl.java +++ b/app/src/test/java/com/alphawallet/app/di/mock/KeyProviderMockNonProductionImpl.java @@ -130,4 +130,16 @@ public String getBlockNativeKey() { return null; } + + @Override + public String getSmartPassKey() + { + return null; + } + + @Override + public String getSmartPassDevKey() + { + return null; + } } diff --git a/app/src/test/java/com/alphawallet/app/viewmodel/HomeViewModelTest.java b/app/src/test/java/com/alphawallet/app/viewmodel/HomeViewModelTest.java index 3b6e0a477d..fa54ad1b4e 100644 --- a/app/src/test/java/com/alphawallet/app/viewmodel/HomeViewModelTest.java +++ b/app/src/test/java/com/alphawallet/app/viewmodel/HomeViewModelTest.java @@ -25,7 +25,7 @@ public class HomeViewModelTest public void setUp() throws Exception { SharedPreferenceRepository sharedPreferenceRepository = new SharedPreferenceRepository(RuntimeEnvironment.getApplication()); - homeViewModel = new HomeViewModel(sharedPreferenceRepository, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + homeViewModel = new HomeViewModel(sharedPreferenceRepository, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); } @Test diff --git a/app/src/test/java/com/alphawallet/shadows/ShadowKeyProviderFactory.java b/app/src/test/java/com/alphawallet/shadows/ShadowKeyProviderFactory.java index 1a1e331b48..de5a886223 100644 --- a/app/src/test/java/com/alphawallet/shadows/ShadowKeyProviderFactory.java +++ b/app/src/test/java/com/alphawallet/shadows/ShadowKeyProviderFactory.java @@ -3,11 +3,9 @@ import com.alphawallet.app.di.mock.KeyProviderMockImpl; import com.alphawallet.app.repository.KeyProvider; import com.alphawallet.app.repository.KeyProviderFactory; -import com.alphawallet.app.repository.KeyProviderJNIImpl; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; -import org.robolectric.annotation.RealObject; @Implements(KeyProviderFactory.class) public class ShadowKeyProviderFactory diff --git a/dmz/src/main/java/com/alphawallet/token/web/AppSiteController.java b/dmz/src/main/java/com/alphawallet/token/web/AppSiteController.java index 0c45ffe03a..d9ec9f625f 100644 --- a/dmz/src/main/java/com/alphawallet/token/web/AppSiteController.java +++ b/dmz/src/main/java/com/alphawallet/token/web/AppSiteController.java @@ -87,8 +87,8 @@ public class AppSiteController implements AttributeInterface " \"namespace\": \"android_app\",\n" + " \"package_name\": \"io.stormbird.wallet\",\n" + " \"sha256_cert_fingerprints\": [\n" + - " \"8E:1E:C7:92:44:E2:AE:8F:5E:BE:A6:09:E5:CC:05:8F:01:9F:67:F4:A6:FF:E7:60:6E:DA:C8:64:8F:29:AB:C0\"\n" + - " \"54:5B:5D:DE:90:45:11:98:14:5C:90:32:C6:AE:F6:85:C3:7D:F5:72:75:FF:25:07:0E:13:03:11:61:66:6A:E3\"\n" + + " \"8E:1E:C7:92:44:E2:AE:8F:5E:BE:A6:09:E5:CC:05:8F:01:9F:67:F4:A6:FF:E7:60:6E:DA:C8:64:8F:29:AB:C0\",\n" + + " \"54:5B:5D:DE:90:45:11:98:14:5C:90:32:C6:AE:F6:85:C3:7D:F5:72:75:FF:25:07:0E:13:03:11:61:66:6A:E3\",\n" + " \"3C:6E:67:6B:7B:9D:AD:53:A3:03:85:CE:E4:53:D4:EC:D8:2A:DC:4B:14:58:4D:55:28:D2:E4:65:57:C3:4F:9D\"\n" + " ]\n" + " }\n" + diff --git a/gradle.properties b/gradle.properties index 9bfedf5d61..fd42c574a2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,6 @@ android.suppressUnsupportedCompileSdk=32 # Make sure only check read:packages and read:user permissions if you want to create your own PAT, # and encode it with Base64 encoder https://www.base64encoder.io/ gpr.user=smarttokenlabs@hotmail.com -gpr.key=Z2hwX3NFR09PbXRkOFQ2M09RcEdmY0xvR1VXTVJ6ODJBSDAxWWQ4OA== +gpr.key=Z2l0aHViX3BhdF8xMUEyMlAyV1kwZ1FNcWpQWlpkd25SXzFUSGViZ1ZPZ3NwZlZmeDhnYjFJc0prTjFNZzVsUmZnZXpEZTlubzFMemFKNDc1RldXU0VhamZ0N3Jj NOTIFICATION_API_BASE_URL="baseurl.here" diff --git a/lib/src/main/java/com/alphawallet/token/entity/AttestationValidationStatus.java b/lib/src/main/java/com/alphawallet/token/entity/AttestationValidationStatus.java index 157ac49a0b..7e14144458 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/AttestationValidationStatus.java +++ b/lib/src/main/java/com/alphawallet/token/entity/AttestationValidationStatus.java @@ -20,6 +20,6 @@ public enum AttestationValidationStatus public String getValue() { - return "Screen: " + validationMessage; + return validationMessage; } } diff --git a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java index 0f0a687d9c..57b40371b5 100644 --- a/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java +++ b/lib/src/main/java/com/alphawallet/token/tools/TokenDefinition.java @@ -267,6 +267,21 @@ public boolean hasEvents() return false; } + //If there's no tokenId input in the call use tokenId 0 + public BigInteger useZeroForTokenIdAgnostic(String attributeName, BigInteger tokenId) + { + Attribute attr = attributes.get(attributeName); + + if (!attr.usesTokenId()) + { + return BigInteger.ZERO; + } + else + { + return tokenId; + } + } + public enum Syntax { DirectoryString, IA5String, Integer, GeneralizedTime, Boolean, BitString, CountryString, JPEG, NumericString