mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-23 12:09:43 +00:00
Integrate EtherScan API for fetching address transactions
Generate Ethereum specific secrets in Ethereum package
This commit is contained in:
parent
fade15d65b
commit
09fd6a8241
14 changed files with 343 additions and 131 deletions
1
.github/workflows/pr_test_build.yml
vendored
1
.github/workflows/pr_test_build.yml
vendored
|
@ -118,6 +118,7 @@ jobs:
|
||||||
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
|
echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart
|
||||||
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
|
echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart
|
||||||
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
|
echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart
|
||||||
|
echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart
|
||||||
|
|
||||||
- name: Rename app
|
- name: Rename app
|
||||||
run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties
|
run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -89,7 +89,9 @@ android/key.properties
|
||||||
**/tool/.secrets-prod.json
|
**/tool/.secrets-prod.json
|
||||||
**/tool/.secrets-test.json
|
**/tool/.secrets-test.json
|
||||||
**/tool/.secrets-config.json
|
**/tool/.secrets-config.json
|
||||||
|
**/tool/.ethereum-secrets-config.json
|
||||||
**/lib/.secrets.g.dart
|
**/lib/.secrets.g.dart
|
||||||
|
**/cw_ethereum/lib/.secrets.g.dart
|
||||||
|
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_ethereum/ethereum_balance.dart';
|
import 'package:cw_ethereum/erc20_balance.dart';
|
||||||
import 'package:cw_core/erc20_token.dart';
|
import 'package:cw_core/erc20_token.dart';
|
||||||
|
import 'package:cw_ethereum/ethereum_transaction_model.dart';
|
||||||
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
|
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
@ -11,6 +13,7 @@ import 'package:web3dart/web3dart.dart';
|
||||||
import 'package:web3dart/contracts/erc20.dart';
|
import 'package:web3dart/contracts/erc20.dart';
|
||||||
import 'package:cw_core/node.dart';
|
import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
|
||||||
|
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
|
||||||
|
|
||||||
class EthereumClient {
|
class EthereumClient {
|
||||||
Web3Client? _client;
|
Web3Client? _client;
|
||||||
|
@ -53,7 +56,6 @@ class EthereumClient {
|
||||||
// final eventFilter = FilterOptions(address: userAddress);
|
// final eventFilter = FilterOptions(address: userAddress);
|
||||||
//
|
//
|
||||||
// _client!.events(eventFilter).listen((event) {
|
// _client!.events(eventFilter).listen((event) {
|
||||||
// print("!!!!!!!!!!!!!!!!!!");
|
|
||||||
// print('Address ${event.address} data ${event.data} tx hash ${event.transactionHash}!');
|
// print('Address ${event.address} data ${event.data} tx hash ${event.transactionHash}!');
|
||||||
// onNewTransaction(event);
|
// onNewTransaction(event);
|
||||||
// });
|
// });
|
||||||
|
@ -61,7 +63,6 @@ class EthereumClient {
|
||||||
// final erc20 = Erc20(client: _client!, address: userAddress);
|
// final erc20 = Erc20(client: _client!, address: userAddress);
|
||||||
//
|
//
|
||||||
// subscription = erc20.transferEvents().take(1).listen((event) {
|
// subscription = erc20.transferEvents().take(1).listen((event) {
|
||||||
// print("!!!!!!!!!!!!!!!!!!");
|
|
||||||
// print('${event.from} sent ${event.value} MetaCoins to ${event.to}!');
|
// print('${event.from} sent ${event.value} MetaCoins to ${event.to}!');
|
||||||
// onNewTransaction(event);
|
// onNewTransaction(event);
|
||||||
// });
|
// });
|
||||||
|
@ -155,7 +156,6 @@ class EthereumClient {
|
||||||
// Wait for the transaction receipt to become available
|
// Wait for the transaction receipt to become available
|
||||||
TransactionReceipt? receipt;
|
TransactionReceipt? receipt;
|
||||||
while (receipt == null) {
|
while (receipt == null) {
|
||||||
print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
|
|
||||||
receipt = await _client!.getTransactionReceipt(transactionHash);
|
receipt = await _client!.getTransactionReceipt(transactionHash);
|
||||||
await Future.delayed(Duration(seconds: 1));
|
await Future.delayed(Duration(seconds: 1));
|
||||||
}
|
}
|
||||||
|
@ -240,6 +240,29 @@ I/flutter ( 4474): Gas Used: 53000
|
||||||
_client?.dispose();
|
_client?.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<EthereumTransactionModel>> fetchTransactions(String address,
|
||||||
|
{String? contractAddress}) async {
|
||||||
|
final client = Client();
|
||||||
|
|
||||||
|
final response = await client.get(Uri.https("api.etherscan.io", "/api", {
|
||||||
|
"module": "account",
|
||||||
|
"action": contractAddress != null ? "tokentx" : "txlist",
|
||||||
|
if (contractAddress != null) "contractaddress": contractAddress,
|
||||||
|
"address": address,
|
||||||
|
"apikey": secrets.etherScanApiKey,
|
||||||
|
}));
|
||||||
|
|
||||||
|
final _jsonResponse = json.decode(response.body) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) {
|
||||||
|
return (_jsonResponse['result'] as List)
|
||||||
|
.map((e) => EthereumTransactionModel.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
|
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
|
||||||
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
|
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
|
||||||
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
|
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
|
||||||
|
|
|
@ -1,27 +1,68 @@
|
||||||
|
import 'package:cw_core/format_amount.dart';
|
||||||
|
import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/transaction_info.dart';
|
import 'package:cw_core/transaction_info.dart';
|
||||||
|
|
||||||
class EthereumTransactionInfo extends TransactionInfo {
|
class EthereumTransactionInfo extends TransactionInfo {
|
||||||
@override
|
EthereumTransactionInfo({
|
||||||
String amountFormatted() {
|
required this.id,
|
||||||
// TODO: implement amountFormatted
|
required this.height,
|
||||||
throw UnimplementedError();
|
required this.amount,
|
||||||
}
|
required this.fee,
|
||||||
|
this.tokenSymbol = "ETH",
|
||||||
|
this.exponent = 18,
|
||||||
|
required this.direction,
|
||||||
|
required this.isPending,
|
||||||
|
required this.date,
|
||||||
|
required this.confirmations,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final int height;
|
||||||
|
final int amount;
|
||||||
|
final int exponent;
|
||||||
|
final TransactionDirection direction;
|
||||||
|
final DateTime date;
|
||||||
|
final bool isPending;
|
||||||
|
final int fee;
|
||||||
|
final int confirmations;
|
||||||
|
final String tokenSymbol;
|
||||||
|
String? _fiatAmount;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void changeFiatAmount(String amount) {
|
String amountFormatted() =>
|
||||||
// TODO: implement changeFiatAmount
|
'${formatAmount((BigInt.from(amount) / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol';
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? feeFormatted() {
|
String fiatAmount() => _fiatAmount ?? '';
|
||||||
// TODO: implement feeFormatted
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String fiatAmount() {
|
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||||
// TODO: implement fiatAmount
|
|
||||||
throw UnimplementedError();
|
@override
|
||||||
|
String feeFormatted() => '${(BigInt.from(fee) / BigInt.from(10).pow(exponent)).toString()} ETH';
|
||||||
|
|
||||||
|
factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) {
|
||||||
|
return EthereumTransactionInfo(
|
||||||
|
id: data['id'] as String,
|
||||||
|
height: data['height'] as int,
|
||||||
|
amount: data['amount'] as int,
|
||||||
|
fee: data['fee'] as int,
|
||||||
|
direction: parseTransactionDirectionFromInt(data['direction'] as int),
|
||||||
|
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
|
||||||
|
isPending: data['isPending'] as bool,
|
||||||
|
confirmations: data['confirmations'] as int);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final m = <String, dynamic>{};
|
||||||
|
m['id'] = id;
|
||||||
|
m['height'] = height;
|
||||||
|
m['amount'] = amount;
|
||||||
|
m['direction'] = direction.index;
|
||||||
|
m['date'] = date.millisecondsSinceEpoch;
|
||||||
|
m['isPending'] = isPending;
|
||||||
|
m['confirmations'] = confirmations;
|
||||||
|
m['fee'] = fee;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
44
cw_ethereum/lib/ethereum_transaction_model.dart
Normal file
44
cw_ethereum/lib/ethereum_transaction_model.dart
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
class EthereumTransactionModel {
|
||||||
|
final DateTime date;
|
||||||
|
final String hash;
|
||||||
|
final String from;
|
||||||
|
final String to;
|
||||||
|
final BigInt amount;
|
||||||
|
final int gasUsed;
|
||||||
|
final BigInt gasPrice;
|
||||||
|
final String contractAddress;
|
||||||
|
final int confirmations;
|
||||||
|
final int blockNumber;
|
||||||
|
final String? tokenSymbol;
|
||||||
|
final int? tokenDecimal;
|
||||||
|
|
||||||
|
EthereumTransactionModel({
|
||||||
|
required this.date,
|
||||||
|
required this.hash,
|
||||||
|
required this.from,
|
||||||
|
required this.to,
|
||||||
|
required this.amount,
|
||||||
|
required this.gasUsed,
|
||||||
|
required this.gasPrice,
|
||||||
|
required this.contractAddress,
|
||||||
|
required this.confirmations,
|
||||||
|
required this.blockNumber,
|
||||||
|
required this.tokenSymbol,
|
||||||
|
required this.tokenDecimal,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory EthereumTransactionModel.fromJson(Map<String, dynamic> json) => EthereumTransactionModel(
|
||||||
|
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
|
||||||
|
hash: json["hash"],
|
||||||
|
from: json["from"],
|
||||||
|
to: json["to"],
|
||||||
|
amount: BigInt.parse(json["value"]),
|
||||||
|
gasUsed: int.parse(json["gasUsed"]),
|
||||||
|
gasPrice: BigInt.parse(json["gasPrice"]),
|
||||||
|
contractAddress: json["contractAddress"],
|
||||||
|
confirmations: int.parse(json["confirmations"]),
|
||||||
|
blockNumber: int.parse(json["blockNumber"]),
|
||||||
|
tokenSymbol: json["tokenSymbol"] ?? "ETH",
|
||||||
|
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,12 +7,13 @@ import 'package:cw_core/node.dart';
|
||||||
import 'package:cw_core/pathForWallet.dart';
|
import 'package:cw_core/pathForWallet.dart';
|
||||||
import 'package:cw_core/pending_transaction.dart';
|
import 'package:cw_core/pending_transaction.dart';
|
||||||
import 'package:cw_core/sync_status.dart';
|
import 'package:cw_core/sync_status.dart';
|
||||||
|
import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/transaction_priority.dart';
|
import 'package:cw_core/transaction_priority.dart';
|
||||||
import 'package:cw_core/wallet_addresses.dart';
|
import 'package:cw_core/wallet_addresses.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_info.dart';
|
import 'package:cw_core/wallet_info.dart';
|
||||||
import 'package:cw_ethereum/default_erc20_tokens.dart';
|
import 'package:cw_ethereum/default_erc20_tokens.dart';
|
||||||
import 'package:cw_ethereum/ethereum_balance.dart';
|
import 'package:cw_ethereum/erc20_balance.dart';
|
||||||
import 'package:cw_ethereum/ethereum_client.dart';
|
import 'package:cw_ethereum/ethereum_client.dart';
|
||||||
import 'package:cw_ethereum/ethereum_exceptions.dart';
|
import 'package:cw_ethereum/ethereum_exceptions.dart';
|
||||||
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
||||||
|
@ -45,6 +46,7 @@ abstract class EthereumWalletBase
|
||||||
_password = password,
|
_password = password,
|
||||||
_mnemonic = mnemonic,
|
_mnemonic = mnemonic,
|
||||||
_priorityFees = [],
|
_priorityFees = [],
|
||||||
|
_isTransactionUpdating = false,
|
||||||
_client = EthereumClient(),
|
_client = EthereumClient(),
|
||||||
walletAddresses = EthereumWalletAddresses(walletInfo),
|
walletAddresses = EthereumWalletAddresses(walletInfo),
|
||||||
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
|
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
|
||||||
|
@ -68,6 +70,7 @@ abstract class EthereumWalletBase
|
||||||
|
|
||||||
List<int> _priorityFees;
|
List<int> _priorityFees;
|
||||||
int? _gasPrice;
|
int? _gasPrice;
|
||||||
|
bool _isTransactionUpdating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
WalletAddresses walletAddresses;
|
WalletAddresses walletAddresses;
|
||||||
|
@ -174,9 +177,56 @@ abstract class EthereumWalletBase
|
||||||
return pendingEthereumTransaction;
|
return pendingEthereumTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> updateTransactions() async {
|
||||||
|
try {
|
||||||
|
if (_isTransactionUpdating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTransactionUpdating = true;
|
||||||
|
final transactions = await fetchTransactions();
|
||||||
|
transactionHistory.addMany(transactions);
|
||||||
|
await transactionHistory.save();
|
||||||
|
_isTransactionUpdating = false;
|
||||||
|
} catch (_) {
|
||||||
|
_isTransactionUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, EthereumTransactionInfo>> fetchTransactions() {
|
Future<Map<String, EthereumTransactionInfo>> fetchTransactions() async {
|
||||||
throw UnimplementedError("fetchTransactions");
|
final address = _privateKey.address.hex;
|
||||||
|
final transactions = await _client.fetchTransactions(address);
|
||||||
|
|
||||||
|
for (var token in balance.keys) {
|
||||||
|
if (token is Erc20Token) {
|
||||||
|
transactions.addAll(await _client.fetchTransactions(
|
||||||
|
address,
|
||||||
|
contractAddress: token.contractAddress,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, EthereumTransactionInfo> result = {};
|
||||||
|
|
||||||
|
for (var transactionModel in transactions) {
|
||||||
|
result[transactionModel.hash] = EthereumTransactionInfo(
|
||||||
|
id: transactionModel.hash,
|
||||||
|
height: transactionModel.blockNumber,
|
||||||
|
amount: transactionModel.amount.toInt(),
|
||||||
|
direction: transactionModel.from == address
|
||||||
|
? TransactionDirection.outgoing
|
||||||
|
: TransactionDirection.incoming,
|
||||||
|
isPending: false,
|
||||||
|
date: transactionModel.date,
|
||||||
|
confirmations: transactionModel.confirmations,
|
||||||
|
fee: transactionModel.gasUsed * transactionModel.gasPrice.toInt(),
|
||||||
|
exponent: transactionModel.tokenDecimal ?? 18,
|
||||||
|
tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -204,6 +254,7 @@ abstract class EthereumWalletBase
|
||||||
try {
|
try {
|
||||||
syncStatus = AttemptingSyncStatus();
|
syncStatus = AttemptingSyncStatus();
|
||||||
await _updateBalance();
|
await _updateBalance();
|
||||||
|
await updateTransactions();
|
||||||
_gasPrice = await _client.getGasUnitPrice();
|
_gasPrice = await _client.getGasUnitPrice();
|
||||||
_priorityFees = await _client.getEstimatedGasForPriorities();
|
_priorityFees = await _client.getEstimatedGasForPriorities();
|
||||||
|
|
||||||
|
@ -354,8 +405,7 @@ abstract class EthereumWalletBase
|
||||||
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
|
||||||
final currentWalletFile = File(currentWalletPath);
|
final currentWalletFile = File(currentWalletPath);
|
||||||
|
|
||||||
final currentDirPath =
|
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
|
||||||
await pathForWalletDir(name: walletInfo.name, type: type);
|
|
||||||
// TODO: un-hash when transactions flow is implemented
|
// TODO: un-hash when transactions flow is implemented
|
||||||
// final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
|
// final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,13 @@ class CWEthereum extends Ethereum {
|
||||||
@override
|
@override
|
||||||
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
|
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double formatterEthereumAmountToDouble({required TransactionInfo transaction}) {
|
||||||
|
transaction as EthereumTransactionInfo;
|
||||||
|
return cryptoAmountToDouble(
|
||||||
|
amount: transaction.amount, divider: BigInt.from(10).pow(transaction.exponent).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Erc20Token> getERC20Currencies(WalletBase wallet) {
|
List<Erc20Token> getERC20Currencies(WalletBase wallet) {
|
||||||
final ethereumWallet = wallet as EthereumWallet;
|
final ethereumWallet = wallet as EthereumWallet;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
import 'package:cake_wallet/entities/balance_display_mode.dart';
|
||||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||||
|
import 'package:cake_wallet/ethereum/ethereum.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
import 'package:cw_core/transaction_direction.dart';
|
import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cw_core/transaction_info.dart';
|
import 'package:cw_core/transaction_info.dart';
|
||||||
|
@ -84,6 +85,11 @@ class TransactionListItem extends ActionListItem with Keyable {
|
||||||
cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount),
|
cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount),
|
||||||
price: price);
|
price: price);
|
||||||
break;
|
break;
|
||||||
|
case WalletType.ethereum:
|
||||||
|
amount = calculateFiatAmountRaw(
|
||||||
|
cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction),
|
||||||
|
price: price);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:cw_core/transaction_direction.dart';
|
||||||
import 'package:cake_wallet/utils/date_formatter.dart';
|
import 'package:cake_wallet/utils/date_formatter.dart';
|
||||||
import 'package:cake_wallet/entities/transaction_description.dart';
|
import 'package:cake_wallet/entities/transaction_description.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:intl/src/intl/date_format.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:cake_wallet/store/settings_store.dart';
|
import 'package:cake_wallet/store/settings_store.dart';
|
||||||
import 'package:cake_wallet/generated/i18n.dart';
|
import 'package:cake_wallet/generated/i18n.dart';
|
||||||
|
@ -27,105 +28,27 @@ abstract class TransactionDetailsViewModelBase with Store {
|
||||||
required this.wallet,
|
required this.wallet,
|
||||||
required this.settingsStore})
|
required this.settingsStore})
|
||||||
: items = [],
|
: items = [],
|
||||||
isRecipientAddressShown = false,
|
isRecipientAddressShown = false,
|
||||||
showRecipientAddress = settingsStore.shouldSaveRecipientAddress {
|
showRecipientAddress = settingsStore.shouldSaveRecipientAddress {
|
||||||
final dateFormat = DateFormatter.withCurrentLocal();
|
final dateFormat = DateFormatter.withCurrentLocal();
|
||||||
final tx = transactionInfo;
|
final tx = transactionInfo;
|
||||||
|
|
||||||
if (wallet.type == WalletType.monero) {
|
switch (wallet.type) {
|
||||||
final key = tx.additionalInfo['key'] as String?;
|
case WalletType.monero:
|
||||||
final accountIndex = tx.additionalInfo['accountIndex'] as int;
|
_addMoneroListItems(tx, dateFormat);
|
||||||
final addressIndex = tx.additionalInfo['addressIndex'] as int;
|
break;
|
||||||
final feeFormatted = tx.feeFormatted();
|
case WalletType.bitcoin:
|
||||||
final _items = [
|
case WalletType.litecoin:
|
||||||
StandartListItem(
|
_addElectrumListItems(tx, dateFormat);
|
||||||
title: S.current.transaction_details_transaction_id, value: tx.id),
|
break;
|
||||||
StandartListItem(
|
case WalletType.haven:
|
||||||
title: S.current.transaction_details_date,
|
_addHavenListItems(tx, dateFormat);
|
||||||
value: dateFormat.format(tx.date)),
|
break;
|
||||||
StandartListItem(
|
case WalletType.ethereum:
|
||||||
title: S.current.transaction_details_height, value: '${tx.height}'),
|
_addEthereumListItems(tx, dateFormat);
|
||||||
StandartListItem(
|
break;
|
||||||
title: S.current.transaction_details_amount,
|
default:
|
||||||
value: tx.amountFormatted()),
|
break;
|
||||||
if (feeFormatted != null)
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_fee, value: feeFormatted),
|
|
||||||
if (key?.isNotEmpty ?? false)
|
|
||||||
StandartListItem(title: S.current.transaction_key, value: key!)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (tx.direction == TransactionDirection.incoming &&
|
|
||||||
accountIndex != null &&
|
|
||||||
addressIndex != null) {
|
|
||||||
try {
|
|
||||||
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
|
|
||||||
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
|
|
||||||
|
|
||||||
if (address?.isNotEmpty ?? false) {
|
|
||||||
isRecipientAddressShown = true;
|
|
||||||
_items.add(
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_recipient_address,
|
|
||||||
value: address));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (label?.isNotEmpty ?? false) {
|
|
||||||
_items.add(
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.address_label,
|
|
||||||
value: label)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print(e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
items.addAll(_items);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallet.type == WalletType.bitcoin
|
|
||||||
|| wallet.type == WalletType.litecoin) {
|
|
||||||
final _items = [
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_transaction_id, value: tx.id),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_date,
|
|
||||||
value: dateFormat.format(tx.date)),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.confirmations,
|
|
||||||
value: tx.confirmations.toString()),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_height, value: '${tx.height}'),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_amount,
|
|
||||||
value: tx.amountFormatted()),
|
|
||||||
if (tx.feeFormatted()?.isNotEmpty ?? false)
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_fee,
|
|
||||||
value: tx.feeFormatted()!),
|
|
||||||
];
|
|
||||||
|
|
||||||
items.addAll(_items);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wallet.type == WalletType.haven) {
|
|
||||||
items.addAll([
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_transaction_id, value: tx.id),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_date,
|
|
||||||
value: dateFormat.format(tx.date)),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_height, value: '${tx.height}'),
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_amount,
|
|
||||||
value: tx.amountFormatted()),
|
|
||||||
if (tx.feeFormatted()?.isNotEmpty ?? false)
|
|
||||||
StandartListItem(
|
|
||||||
title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showRecipientAddress && !isRecipientAddressShown) {
|
if (showRecipientAddress && !isRecipientAddressShown) {
|
||||||
|
@ -136,10 +59,9 @@ abstract class TransactionDetailsViewModelBase with Store {
|
||||||
|
|
||||||
if (recipientAddress?.isNotEmpty ?? false) {
|
if (recipientAddress?.isNotEmpty ?? false) {
|
||||||
items.add(StandartListItem(
|
items.add(StandartListItem(
|
||||||
title: S.current.transaction_details_recipient_address,
|
title: S.current.transaction_details_recipient_address, value: recipientAddress!));
|
||||||
value: recipientAddress!));
|
|
||||||
}
|
}
|
||||||
} catch(_) {
|
} catch (_) {
|
||||||
// FIX-ME: Unhandled exception
|
// FIX-ME: Unhandled exception
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,4 +133,86 @@ abstract class TransactionDetailsViewModelBase with Store {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||||
|
final key = tx.additionalInfo['key'] as String?;
|
||||||
|
final accountIndex = tx.additionalInfo['accountIndex'] as int;
|
||||||
|
final addressIndex = tx.additionalInfo['addressIndex'] as int;
|
||||||
|
final feeFormatted = tx.feeFormatted();
|
||||||
|
final _items = [
|
||||||
|
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
|
||||||
|
StandartListItem(
|
||||||
|
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
|
||||||
|
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||||
|
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
|
||||||
|
if (feeFormatted != null)
|
||||||
|
StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted),
|
||||||
|
if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (tx.direction == TransactionDirection.incoming) {
|
||||||
|
try {
|
||||||
|
final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex);
|
||||||
|
final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex);
|
||||||
|
|
||||||
|
if (address.isNotEmpty) {
|
||||||
|
isRecipientAddressShown = true;
|
||||||
|
_items.add(StandartListItem(
|
||||||
|
title: S.current.transaction_details_recipient_address,
|
||||||
|
value: address,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (label.isNotEmpty) {
|
||||||
|
_items.add(StandartListItem(title: S.current.address_label, value: label));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.addAll(_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||||
|
final _items = [
|
||||||
|
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
|
||||||
|
StandartListItem(
|
||||||
|
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
|
||||||
|
StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()),
|
||||||
|
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||||
|
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
|
||||||
|
if (tx.feeFormatted()?.isNotEmpty ?? false)
|
||||||
|
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
|
||||||
|
];
|
||||||
|
|
||||||
|
items.addAll(_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||||
|
items.addAll([
|
||||||
|
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
|
||||||
|
StandartListItem(
|
||||||
|
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
|
||||||
|
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||||
|
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
|
||||||
|
if (tx.feeFormatted()?.isNotEmpty ?? false)
|
||||||
|
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addEthereumListItems(TransactionInfo tx, DateFormat dateFormat) {
|
||||||
|
final _items = [
|
||||||
|
StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id),
|
||||||
|
StandartListItem(
|
||||||
|
title: S.current.transaction_details_date, value: dateFormat.format(tx.date)),
|
||||||
|
StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()),
|
||||||
|
StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'),
|
||||||
|
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
|
||||||
|
if (tx.feeFormatted()?.isNotEmpty ?? false)
|
||||||
|
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
|
||||||
|
];
|
||||||
|
|
||||||
|
items.addAll(_items);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -480,9 +480,11 @@ Future<void> generateEthereum(bool hasImplementation) async {
|
||||||
""";
|
""";
|
||||||
const ethereumCWHeaders = """
|
const ethereumCWHeaders = """
|
||||||
import 'package:cake_wallet/view_model/send/output.dart';
|
import 'package:cake_wallet/view_model/send/output.dart';
|
||||||
|
import 'package:cw_core/crypto_amount_format.dart';
|
||||||
import 'package:cw_core/crypto_currency.dart';
|
import 'package:cw_core/crypto_currency.dart';
|
||||||
import 'package:cw_core/erc20_token.dart';
|
import 'package:cw_core/erc20_token.dart';
|
||||||
import 'package:cw_core/output_info.dart';
|
import 'package:cw_core/output_info.dart';
|
||||||
|
import 'package:cw_core/transaction_info.dart';
|
||||||
import 'package:cw_core/transaction_priority.dart';
|
import 'package:cw_core/transaction_priority.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
import 'package:cw_core/wallet_credentials.dart';
|
import 'package:cw_core/wallet_credentials.dart';
|
||||||
|
@ -491,6 +493,7 @@ import 'package:cw_core/wallet_service.dart';
|
||||||
import 'package:cw_ethereum/ethereum_formatter.dart';
|
import 'package:cw_ethereum/ethereum_formatter.dart';
|
||||||
import 'package:cw_ethereum/ethereum_mnemonics.dart';
|
import 'package:cw_ethereum/ethereum_mnemonics.dart';
|
||||||
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
|
||||||
|
import 'package:cw_ethereum/ethereum_transaction_info.dart';
|
||||||
import 'package:cw_ethereum/ethereum_wallet.dart';
|
import 'package:cw_ethereum/ethereum_wallet.dart';
|
||||||
import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart';
|
import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart';
|
||||||
import 'package:cw_ethereum/ethereum_wallet_service.dart';
|
import 'package:cw_ethereum/ethereum_wallet_service.dart';
|
||||||
|
@ -525,6 +528,7 @@ abstract class Ethereum {
|
||||||
});
|
});
|
||||||
|
|
||||||
int formatterEthereumParseAmount(String amount);
|
int formatterEthereumParseAmount(String amount);
|
||||||
|
double formatterEthereumAmountToDouble({required TransactionInfo transaction});
|
||||||
List<Erc20Token> getERC20Currencies(WalletBase wallet);
|
List<Erc20Token> getERC20Currencies(WalletBase wallet);
|
||||||
Future<void> addErc20Token(WalletBase wallet, Erc20Token token);
|
Future<void> addErc20Token(WalletBase wallet, Erc20Token token);
|
||||||
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token);
|
Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token);
|
||||||
|
|
|
@ -4,12 +4,12 @@ import 'utils/secret_key.dart';
|
||||||
import 'utils/utils.dart';
|
import 'utils/utils.dart';
|
||||||
|
|
||||||
const configPath = 'tool/.secrets-config.json';
|
const configPath = 'tool/.secrets-config.json';
|
||||||
|
const ethereumConfigPath = 'tool/.ethereum-secrets-config.json';
|
||||||
|
|
||||||
Future<void> main(List<String> args) async => generateSecretsConfig(args);
|
Future<void> main(List<String> args) async => generateSecretsConfig(args);
|
||||||
|
|
||||||
Future<void> generateSecretsConfig(List<String> args) async {
|
Future<void> generateSecretsConfig(List<String> args) async {
|
||||||
final extraInfo =
|
final extraInfo = args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
||||||
args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) {
|
|
||||||
final parts = arg.split('=');
|
final parts = arg.split('=');
|
||||||
final key = normalizeKeyName(parts[0]);
|
final key = normalizeKeyName(parts[0]);
|
||||||
acc[key] = acc[key] = parts.length > 1 ? parts[1] : 1;
|
acc[key] = acc[key] = parts.length > 1 ? parts[1] : 1;
|
||||||
|
@ -17,6 +17,7 @@ Future<void> generateSecretsConfig(List<String> args) async {
|
||||||
});
|
});
|
||||||
|
|
||||||
final configFile = File(configPath);
|
final configFile = File(configPath);
|
||||||
|
final ethereumConfigFile = File(ethereumConfigPath);
|
||||||
final secrets = <String, dynamic>{};
|
final secrets = <String, dynamic>{};
|
||||||
|
|
||||||
secrets.addAll(extraInfo);
|
secrets.addAll(extraInfo);
|
||||||
|
@ -44,6 +45,19 @@ Future<void> generateSecretsConfig(List<String> args) async {
|
||||||
secrets[sec.name] = sec.generate();
|
secrets[sec.name] = sec.generate();
|
||||||
});
|
});
|
||||||
|
|
||||||
final secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
|
var secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
|
||||||
await configFile.writeAsString(secretsJson);
|
await configFile.writeAsString(secretsJson);
|
||||||
|
|
||||||
|
secrets.clear();
|
||||||
|
SecretKey.ethereumSecrets.forEach((sec) {
|
||||||
|
if (secrets[sec.name] != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets[sec.name] = sec.generate();
|
||||||
|
});
|
||||||
|
|
||||||
|
secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
|
||||||
|
|
||||||
|
await ethereumConfigFile.writeAsString(secretsJson);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,31 @@ import 'utils/utils.dart';
|
||||||
const configPath = 'tool/.secrets-config.json';
|
const configPath = 'tool/.secrets-config.json';
|
||||||
const outputPath = 'lib/.secrets.g.dart';
|
const outputPath = 'lib/.secrets.g.dart';
|
||||||
|
|
||||||
|
const ethereumConfigPath = 'tool/.ethereum-secrets-config.json';
|
||||||
|
const ethereumOutputPath = 'cw_ethereum/lib/.secrets.g.dart';
|
||||||
|
|
||||||
Future<void> main(List<String> args) async => importSecretsConfig();
|
Future<void> main(List<String> args) async => importSecretsConfig();
|
||||||
|
|
||||||
Future<void> importSecretsConfig() async {
|
Future<void> importSecretsConfig() async {
|
||||||
final outputFile = File(outputPath);
|
final outputFile = File(outputPath);
|
||||||
final input = json.decode(File(configPath).readAsStringSync())
|
final input = json.decode(File(configPath).readAsStringSync()) as Map<String, dynamic>;
|
||||||
as Map<String, dynamic> ??
|
final output = input.keys.fold('', (String acc, String val) => acc + generateConst(val, input));
|
||||||
<String, dynamic>{};
|
|
||||||
final output = input.keys
|
final ethereumOutputFile = File(ethereumOutputPath);
|
||||||
.fold('', (String acc, String val) => acc + generateConst(val, input));
|
final ethereumInput =
|
||||||
|
json.decode(File(ethereumConfigPath).readAsStringSync()) as Map<String, dynamic>;
|
||||||
|
final ethereumOutput = ethereumInput.keys
|
||||||
|
.fold('', (String acc, String val) => acc + generateConst(val, ethereumInput));
|
||||||
|
|
||||||
if (outputFile.existsSync()) {
|
if (outputFile.existsSync()) {
|
||||||
await outputFile.delete();
|
await outputFile.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
await outputFile.writeAsString(output);
|
await outputFile.writeAsString(output);
|
||||||
|
|
||||||
|
if (ethereumOutputFile.existsSync()) {
|
||||||
|
await ethereumOutputFile.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
await ethereumOutputFile.writeAsString(ethereumOutput);
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ class SecretKey {
|
||||||
SecretKey('payfuraApiKey', () => ''),
|
SecretKey('payfuraApiKey', () => ''),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static final ethereumSecrets = [
|
||||||
|
SecretKey('etherScanApiKey', () => ''),
|
||||||
|
];
|
||||||
|
|
||||||
final String name;
|
final String name;
|
||||||
final String Function() generate;
|
final String Function() generate;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue