mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
feat: Add warning before adding erc20 token via contract address (#1663)
* feat: Add warning before adding erc20 token via contract address * fix: Update message on edit token page * fix: Adjust polygon secrets variable name * fix: Handle loading state, translate strings and fix other issues * fix linux build [skip ci] * fix: Modify warning text for contract address scam alert * Update secret_key.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Omar Hatem
parent
0e106834bc
commit
7bf2fb9dce
@@ -0,0 +1,80 @@
|
||||
class Erc20TokenInfoExplorers {
|
||||
String? contractAddress;
|
||||
String? tokenName;
|
||||
String? symbol;
|
||||
String? divisor;
|
||||
String? tokenType;
|
||||
String? totalSupply;
|
||||
String? blueCheckmark;
|
||||
String? description;
|
||||
String? website;
|
||||
String? email;
|
||||
String? blog;
|
||||
String? reddit;
|
||||
String? slack;
|
||||
String? facebook;
|
||||
String? twitter;
|
||||
String? bitcointalk;
|
||||
String? github;
|
||||
String? telegram;
|
||||
String? wechat;
|
||||
String? linkedin;
|
||||
String? discord;
|
||||
String? whitepaper;
|
||||
String? tokenPriceUSD;
|
||||
String? image;
|
||||
|
||||
Erc20TokenInfoExplorers({
|
||||
this.contractAddress,
|
||||
this.tokenName,
|
||||
this.symbol,
|
||||
this.divisor,
|
||||
this.tokenType,
|
||||
this.totalSupply,
|
||||
this.blueCheckmark,
|
||||
this.description,
|
||||
this.website,
|
||||
this.email,
|
||||
this.blog,
|
||||
this.reddit,
|
||||
this.slack,
|
||||
this.facebook,
|
||||
this.twitter,
|
||||
this.bitcointalk,
|
||||
this.github,
|
||||
this.telegram,
|
||||
this.wechat,
|
||||
this.linkedin,
|
||||
this.discord,
|
||||
this.whitepaper,
|
||||
this.tokenPriceUSD,
|
||||
this.image,
|
||||
});
|
||||
|
||||
Erc20TokenInfoExplorers.fromJson(Map<String, dynamic> json) {
|
||||
contractAddress = json['contractAddress'] as String?;
|
||||
tokenName = json['tokenName'] as String?;
|
||||
symbol = json['symbol'] as String?;
|
||||
divisor = json['divisor'] as String?;
|
||||
tokenType = json['tokenType'] as String?;
|
||||
totalSupply = json['totalSupply'] as String?;
|
||||
blueCheckmark = json['blueCheckmark'] as String?;
|
||||
description = json['description'] as String?;
|
||||
website = json['website'] as String?;
|
||||
email = json['email'] as String?;
|
||||
blog = json['blog'] as String?;
|
||||
reddit = json['reddit'] as String?;
|
||||
slack = json['slack'] as String?;
|
||||
facebook = json['facebook'] as String?;
|
||||
twitter = json['twitter'] as String?;
|
||||
bitcointalk = json['bitcointalk'] as String?;
|
||||
github = json['github'] as String?;
|
||||
telegram = json['telegram'] as String?;
|
||||
wechat = json['wechat'] as String?;
|
||||
linkedin = json['linkedin'] as String?;
|
||||
discord = json['discord'] as String?;
|
||||
whitepaper = json['whitepaper'] as String?;
|
||||
tokenPriceUSD = json['tokenPriceUSD'] as String?;
|
||||
image = json['image'] as String?;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
class Erc20TokenInfoMoralis {
|
||||
String? address;
|
||||
String? addressLabel;
|
||||
String? name;
|
||||
String? symbol;
|
||||
String? decimals;
|
||||
String? logo;
|
||||
String? logoHash;
|
||||
String? thumbnail;
|
||||
String? totalSupply;
|
||||
String? totalSupplyFormatted;
|
||||
String? fullyDilutedValuation;
|
||||
String? blockNumber;
|
||||
int? validated;
|
||||
String? createdAt;
|
||||
bool? possibleSpam;
|
||||
bool? verifiedContract;
|
||||
Links? links;
|
||||
int? securityScore;
|
||||
|
||||
Erc20TokenInfoMoralis({
|
||||
this.address,
|
||||
this.addressLabel,
|
||||
this.name,
|
||||
this.symbol,
|
||||
this.decimals,
|
||||
this.logo,
|
||||
this.logoHash,
|
||||
this.thumbnail,
|
||||
this.totalSupply,
|
||||
this.totalSupplyFormatted,
|
||||
this.fullyDilutedValuation,
|
||||
this.blockNumber,
|
||||
this.validated,
|
||||
this.createdAt,
|
||||
this.possibleSpam,
|
||||
this.verifiedContract,
|
||||
this.links,
|
||||
this.securityScore,
|
||||
});
|
||||
|
||||
Erc20TokenInfoMoralis.fromJson(Map<String, dynamic> json) {
|
||||
address = json['address'] as String?;
|
||||
addressLabel = json['address_label'] as String?;
|
||||
name = json['name'] as String?;
|
||||
symbol = json['symbol'] as String?;
|
||||
decimals = json['decimals'] as String?;
|
||||
logo = json['logo'] as String?;
|
||||
logoHash = json['logo_hash'] as String?;
|
||||
thumbnail = json['thumbnail'] as String?;
|
||||
totalSupply = json['total_supply'] as String?;
|
||||
totalSupplyFormatted = json['total_supply_formatted'] as String?;
|
||||
fullyDilutedValuation = json['fully_diluted_valuation'] as String?;
|
||||
blockNumber = json['block_number'] as String?;
|
||||
validated = json['validated'] as int?;
|
||||
createdAt = json['created_at'] as String?;
|
||||
possibleSpam = json['possible_spam'] as bool?;
|
||||
verifiedContract = json['verified_contract'] as bool;
|
||||
links =
|
||||
json['links'] != null ? new Links.fromJson(json['links'] as Map<String, dynamic>) : null;
|
||||
securityScore = json['security_score'] as int?;
|
||||
}
|
||||
}
|
||||
|
||||
class Links {
|
||||
String? twitter;
|
||||
String? website;
|
||||
String? facebook;
|
||||
String? reddit;
|
||||
String? github;
|
||||
String? linkedin;
|
||||
String? telegram;
|
||||
|
||||
Links({this.twitter, this.website, this.facebook, this.reddit});
|
||||
|
||||
Links.fromJson(Map<String, dynamic> json) {
|
||||
twitter = json['twitter'] as String?;
|
||||
website = json['website'] as String?;
|
||||
facebook = json['facebook'] as String?;
|
||||
reddit = json['reddit'] as String?;
|
||||
github = json['github'] as String?;
|
||||
linkedin = json['linkedin'] as String?;
|
||||
telegram = json['telegram'] as String?;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,19 @@ import 'package:cake_wallet/core/address_validator.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/checkbox_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
|
||||
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
|
||||
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class EditTokenPage extends BasePage {
|
||||
EditTokenPage({
|
||||
@@ -79,7 +82,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
|
||||
if (widget.token != null) {
|
||||
address = widget.homeSettingsViewModel.getTokenAddressBasedOnWallet(widget.token!);
|
||||
|
||||
|
||||
_contractAddressController.text = address ?? '';
|
||||
_tokenNameController.text = widget.token!.name;
|
||||
_tokenSymbolController.text = widget.token!.title;
|
||||
@@ -135,8 +138,8 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
S.of(context).warning,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).dialogTheme.backgroundColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
@@ -145,7 +148,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
S.of(context).add_token_warning,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context)
|
||||
.extension<TransactionTradeTheme>()!
|
||||
.detailsTitlesColor,
|
||||
@@ -167,6 +170,15 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
if (_showDisclaimer) ...[
|
||||
Text(
|
||||
S.current.do_not_send_funds_to_contract_address_warning,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.0,
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
CheckboxWidget(
|
||||
value: _disclaimerChecked,
|
||||
caption: S.of(context).add_token_disclaimer_check,
|
||||
@@ -176,47 +188,84 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (widget.token != null) {
|
||||
await widget.homeSettingsViewModel.deleteToken(widget.token!);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
},
|
||||
text: widget.token != null ? S.of(context).delete : S.of(context).cancel,
|
||||
color: Colors.red,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
(!_showDisclaimer || _disclaimerChecked)) {
|
||||
await widget.homeSettingsViewModel.addToken(
|
||||
token: CryptoCurrency(
|
||||
name: _tokenNameController.text,
|
||||
title: _tokenSymbolController.text.toUpperCase(),
|
||||
decimals: int.parse(_tokenDecimalController.text),
|
||||
iconPath: _tokenIconPathController.text,
|
||||
),
|
||||
contractAddress: _contractAddressController.text,
|
||||
);
|
||||
if (mounted) {
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: LoadingPrimaryButton(
|
||||
isLoading: widget.homeSettingsViewModel.isDeletingToken,
|
||||
onPressed: () async {
|
||||
if (widget.token != null) {
|
||||
await widget.homeSettingsViewModel.deleteToken(widget.token!);
|
||||
}
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).primaryColor,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
},
|
||||
text: widget.token != null ? S.of(context).delete : S.of(context).cancel,
|
||||
color: Colors.red,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: LoadingPrimaryButton(
|
||||
isLoading: widget.homeSettingsViewModel.isAddingToken ||
|
||||
widget.homeSettingsViewModel.isValidatingContractAddress,
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState!.validate() &&
|
||||
(!_showDisclaimer || _disclaimerChecked)) {
|
||||
final hasPotentialError = await widget.homeSettingsViewModel
|
||||
.checkIfERC20TokenContractAddressIsAPotentialScamAddress(
|
||||
_contractAddressController.text,
|
||||
);
|
||||
final actionCall = () async {
|
||||
await widget.homeSettingsViewModel.addToken(
|
||||
token: CryptoCurrency(
|
||||
name: _tokenNameController.text,
|
||||
title: _tokenSymbolController.text.toUpperCase(),
|
||||
decimals: int.parse(_tokenDecimalController.text),
|
||||
iconPath: _tokenIconPathController.text,
|
||||
),
|
||||
contractAddress: _contractAddressController.text,
|
||||
);
|
||||
};
|
||||
|
||||
if (hasPotentialError) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.current.warning,
|
||||
alertContent: S.current.contract_warning,
|
||||
rightButtonText: S.of(context).continue_text,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () async {
|
||||
Navigator.of(dialogContext).pop();
|
||||
await actionCall();
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await actionCall();
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).primaryColor,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
import 'package:cake_wallet/entities/erc20_token_info_explorers.dart';
|
||||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/erc20_token_info_moralis.dart';
|
||||
import 'package:cake_wallet/entities/sort_balance_types.dart';
|
||||
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||
import 'package:cake_wallet/polygon/polygon.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/tron/tron.dart';
|
||||
@@ -11,6 +17,8 @@ import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/erc20_token.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
|
||||
part 'home_settings_view_model.g.dart';
|
||||
|
||||
@@ -18,7 +26,10 @@ class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewM
|
||||
|
||||
abstract class HomeSettingsViewModelBase with Store {
|
||||
HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel)
|
||||
: tokens = ObservableSet<CryptoCurrency>() {
|
||||
: tokens = ObservableSet<CryptoCurrency>(),
|
||||
isAddingToken = false,
|
||||
isDeletingToken = false,
|
||||
isValidatingContractAddress = false {
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
@@ -27,6 +38,15 @@ abstract class HomeSettingsViewModelBase with Store {
|
||||
|
||||
final ObservableSet<CryptoCurrency> tokens;
|
||||
|
||||
@observable
|
||||
bool isAddingToken;
|
||||
|
||||
@observable
|
||||
bool isDeletingToken;
|
||||
|
||||
@observable
|
||||
bool isValidatingContractAddress;
|
||||
|
||||
@observable
|
||||
String searchText = '';
|
||||
|
||||
@@ -45,66 +65,263 @@ abstract class HomeSettingsViewModelBase with Store {
|
||||
@action
|
||||
void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value;
|
||||
|
||||
@action
|
||||
Future<void> addToken({
|
||||
required String contractAddress,
|
||||
required CryptoCurrency token,
|
||||
}) async {
|
||||
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
|
||||
final erc20token = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.title,
|
||||
decimal: token.decimals,
|
||||
contractAddress: contractAddress,
|
||||
iconPath: token.iconPath,
|
||||
);
|
||||
try {
|
||||
isAddingToken = true;
|
||||
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
|
||||
final erc20token = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.title,
|
||||
decimal: token.decimals,
|
||||
contractAddress: contractAddress,
|
||||
iconPath: token.iconPath,
|
||||
);
|
||||
|
||||
await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token);
|
||||
await ethereum!.addErc20Token(_balanceViewModel.wallet, erc20token);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.polygon) {
|
||||
final polygonToken = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.title,
|
||||
decimal: token.decimals,
|
||||
contractAddress: contractAddress,
|
||||
iconPath: token.iconPath,
|
||||
);
|
||||
await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||
await solana!.addSPLToken(
|
||||
_balanceViewModel.wallet,
|
||||
token,
|
||||
contractAddress,
|
||||
);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
||||
await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress);
|
||||
}
|
||||
|
||||
_updateTokensList();
|
||||
_updateFiatPrices(token);
|
||||
} finally {
|
||||
isAddingToken = false;
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.polygon) {
|
||||
final polygonToken = Erc20Token(
|
||||
name: token.name,
|
||||
symbol: token.title,
|
||||
decimal: token.decimals,
|
||||
contractAddress: contractAddress,
|
||||
iconPath: token.iconPath,
|
||||
);
|
||||
await polygon!.addErc20Token(_balanceViewModel.wallet, polygonToken);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||
await solana!.addSPLToken(
|
||||
_balanceViewModel.wallet,
|
||||
token,
|
||||
contractAddress,
|
||||
);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
||||
await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress);
|
||||
}
|
||||
|
||||
_updateTokensList();
|
||||
_updateFiatPrices(token);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> deleteToken(CryptoCurrency token) async {
|
||||
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
|
||||
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
|
||||
}
|
||||
try {
|
||||
isDeletingToken = true;
|
||||
if (_balanceViewModel.wallet.type == WalletType.ethereum) {
|
||||
await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.polygon) {
|
||||
await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
|
||||
}
|
||||
if (_balanceViewModel.wallet.type == WalletType.polygon) {
|
||||
await polygon!.deleteErc20Token(_balanceViewModel.wallet, token as Erc20Token);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||
await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
|
||||
}
|
||||
if (_balanceViewModel.wallet.type == WalletType.solana) {
|
||||
await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
|
||||
}
|
||||
|
||||
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
||||
await tron!.deleteTronToken(_balanceViewModel.wallet, token);
|
||||
if (_balanceViewModel.wallet.type == WalletType.tron) {
|
||||
await tron!.deleteTronToken(_balanceViewModel.wallet, token);
|
||||
}
|
||||
_updateTokensList();
|
||||
} finally {
|
||||
isDeletingToken = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkIfERC20TokenContractAddressIsAPotentialScamAddress(
|
||||
String contractAddress,
|
||||
) async {
|
||||
try {
|
||||
isValidatingContractAddress = true;
|
||||
|
||||
if (!isEVMCompatibleChain(_balanceViewModel.wallet.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isEthereum = _balanceViewModel.wallet.type == WalletType.ethereum;
|
||||
|
||||
bool isPotentialScamViaMoralis = await _isPotentialScamTokenViaMoralis(
|
||||
contractAddress,
|
||||
isEthereum ? 'eth' : 'polygon',
|
||||
);
|
||||
|
||||
bool isPotentialScamViaExplorers = await _isPotentialScamTokenViaExplorers(
|
||||
contractAddress,
|
||||
isEthereum: isEthereum,
|
||||
);
|
||||
|
||||
bool isUnverifiedContract = await _isContractUnverified(
|
||||
contractAddress,
|
||||
isEthereum: isEthereum,
|
||||
);
|
||||
|
||||
final showWarningForContractAddress =
|
||||
isPotentialScamViaMoralis || isUnverifiedContract || isPotentialScamViaExplorers;
|
||||
|
||||
return showWarningForContractAddress;
|
||||
} finally {
|
||||
isValidatingContractAddress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isPotentialScamTokenViaMoralis(
|
||||
String contractAddress,
|
||||
String chainName,
|
||||
) async {
|
||||
final uri = Uri.https(
|
||||
'deep-index.moralis.io',
|
||||
'/api/v2.2/erc20/metadata',
|
||||
{
|
||||
"chain": chainName,
|
||||
"addresses": contractAddress,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"X-API-Key": secrets.moralisApiKey,
|
||||
},
|
||||
);
|
||||
|
||||
final decodedResponse = jsonDecode(response.body);
|
||||
|
||||
final tokenInfo = Erc20TokenInfoMoralis.fromJson(decodedResponse[0] as Map<String, dynamic>);
|
||||
|
||||
// Based on analysis using Moralis internal metrics
|
||||
if (tokenInfo.possibleSpam == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tokens whose contract have not been verified are potentially risky tokens.
|
||||
if (tokenInfo.verifiedContract == false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Tokens with a security score less than 40 are potentially risky, requiring caution when dealing with them.
|
||||
if (tokenInfo.securityScore == null || tokenInfo.securityScore! < 40) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Absence of a website URL for an ERC-20 token can be a potential red flag. A legitimate ERC-20 projects should have a well-maintained website that provides information about the token, its purpose, team, and roadmap.
|
||||
if (tokenInfo.links?.website == null || tokenInfo.links!.website!.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Having a Fully Diluted Valiuation of 0 is a significant red flag that could signify:
|
||||
// - An abandoned/unlaunched project
|
||||
// - Incorrect/missing token data
|
||||
// - Suspicious manipulation of token data
|
||||
if (tokenInfo.fullyDilutedValuation == '0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// I mean, a logo is the most basic of all the potential causes, but why does your fully functional project not have a logo?
|
||||
if (tokenInfo.logo == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isPotentialScamTokenViaExplorers(
|
||||
String contractAddress, {
|
||||
required bool isEthereum,
|
||||
}) async {
|
||||
final uri = Uri.https(
|
||||
isEthereum ? "api.etherscan.io" : "api.polygonscan.com",
|
||||
"/api",
|
||||
{
|
||||
"module": "token",
|
||||
"action": "tokeninfo",
|
||||
"contractaddress": contractAddress,
|
||||
"apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(uri);
|
||||
|
||||
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (decodedResponse['status'] != '1') {
|
||||
log('${decodedResponse['result']}');
|
||||
return true;
|
||||
}
|
||||
|
||||
final tokenInfo =
|
||||
Erc20TokenInfoExplorers.fromJson(decodedResponse['result'][0] as Map<String, dynamic>);
|
||||
|
||||
// A token without an email to reach its creators is a potential red flag
|
||||
if (tokenInfo.email?.isEmpty == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A token without a website is a potential red flag
|
||||
if (tokenInfo.website?.isEmpty == true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if (tokenInfo.whitepaper == null) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _isContractUnverified(
|
||||
String contractAddress, {
|
||||
required bool isEthereum,
|
||||
}) async {
|
||||
final uri = Uri.https(
|
||||
isEthereum ? "api.etherscan.io" : "api.polygonscan.com",
|
||||
"/api",
|
||||
{
|
||||
"module": "contract",
|
||||
"action": "getsourcecode",
|
||||
"contractaddress": contractAddress,
|
||||
"apikey": isEthereum ? secrets.etherScanApiKey : secrets.polygonScanApiKey,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
final response = await http.get(uri);
|
||||
|
||||
final decodedResponse = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
|
||||
if (decodedResponse['status'] == '0') {
|
||||
log('${decodedResponse['result']}');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (decodedResponse['status'] == '1' &&
|
||||
decodedResponse['result'][0]['ABI'] == 'Contract source code not verified') {
|
||||
return true; // Contract is not verified
|
||||
} else {
|
||||
return false; // Contract is verified
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
_updateTokensList();
|
||||
}
|
||||
|
||||
Future<CryptoCurrency?> getToken(String contractAddress) async {
|
||||
|
||||
Reference in New Issue
Block a user