Merge branch 'main' into CW-537-Integrate-ThorChain-swaps

This commit is contained in:
Serhii 2024-02-04 16:54:16 +02:00
commit 5ac5f7ef00
103 changed files with 1880 additions and 2131 deletions

View file

@ -105,21 +105,21 @@ jobs:
run: | run: |
cd /opt/android/cake_wallet cd /opt/android/cake_wallet
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && cd ..
cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs flutter packages pub run build_runner build --delete-conflicting-outputs
- name: Add secrets - name: Add secrets
run: | run: |
cd /opt/android/cake_wallet cd /opt/android/cake_wallet
touch lib/.secrets.g.dart touch lib/.secrets.g.dart
touch cw_ethereum/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart
touch cw_polygon/lib/.secrets.g.dart
echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@ -146,14 +146,14 @@ 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 echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart
echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart
echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart
echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart
echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart
- name: Rename app - name: Rename app
run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties

3
.gitignore vendored
View file

@ -90,9 +90,10 @@ 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/.evm-secrets-config.json
**/tool/.ethereum-secrets-config.json **/tool/.ethereum-secrets-config.json
**/lib/.secrets.g.dart **/lib/.secrets.g.dart
**/cw_ethereum/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart
vendor/ vendor/

View file

@ -1,3 +1,3 @@
On-ramp flow fixes and enhancements Security and Privacy enhancements
UI enhancements Usability enhancements
Generic enhancements and bug fixes Bug fixes

View file

@ -1,6 +1,4 @@
Add new Off-ramp providers (DFX, OnRamper) List previously used Bitcoin addresses
On-ramp flow fixes and enhancements Security and Privacy enhancements
Ethereum and WalletConnect fixes and improvements Usability enhancements
Nano enhancements Bug fixes
UI enhancements
Generic enhancements and bug fixes

View file

@ -24,11 +24,12 @@ source ./app_env.sh cakewallet
cd ../.. && flutter pub get cd ../.. && flutter pub get
flutter packages pub run tool/generate_localization.dart flutter packages pub run tool/generate_localization.dart
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && cd ..
cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -1,7 +1,7 @@
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';
class DefaultErc20Tokens { class DefaultEthereumErc20Tokens {
final List<Erc20Token> _defaultTokens = [ final List<Erc20Token> _defaultTokens = [
Erc20Token( Erc20Token(
name: "USD Coin", name: "USD Coin",

View file

@ -1,241 +1,22 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer';
import 'dart:typed_data';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_evm/evm_chain_client.dart';
import 'package:hex/hex.dart' as hex; import 'package:cw_evm/.secrets.g.dart' as secrets;
import 'package:cw_ethereum/erc20_balance.dart'; import 'package:cw_evm/evm_chain_transaction_model.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:flutter/services.dart';
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
import 'package:erc20/erc20.dart';
import 'package:cw_core/node.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
import 'package:cw_ethereum/.secrets.g.dart' as secrets;
class EthereumClient {
final httpClient = Client();
Web3Client? _client;
bool connect(Node node) {
try {
_client = Web3Client(node.uri.toString(), httpClient);
return true;
} catch (e) {
return false;
}
}
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
// _client?.pendingTransactions().listen((transactionHash) async {
// final transaction = await _client!.getTransactionByHash(transactionHash);
//
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
// onNewTransaction();
// }
// });
}
Future<EtherAmount> getBalance(EthereumAddress address) async {
try {
return await _client!.getBalance(address);
} catch (_) {
return EtherAmount.zero();
}
}
Future<int> getGasUnitPrice() async {
try {
final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> getEstimatedGas() async {
try {
final estimatedGas = await _client!.estimateGas();
return estimatedGas.toInt();
} catch (_) {
return 0;
}
}
Future<PendingEthereumTransaction> signTransaction({
required EthPrivateKey privateKey,
required String toAddress,
required String amount,
required int gas,
required EthereumTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
String? contractAddress,
String? data,
}) async {
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
contractAddress != null);
bool _isEVMCompatibleChain =
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null,
);
final signedTransaction =
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
final Function _sendTransaction;
if (_isEVMCompatibleChain) {
_sendTransaction = () async => await sendTransaction(signedTransaction);
} else {
final erc20 = ERC20(
client: _client!,
address: EthereumAddress.fromHex(contractAddress!),
chainId: chainId,
);
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
BigInt.parse(amount),
credentials: privateKey,
transaction: transaction,
);
};
}
return PendingEthereumTransaction(
signedTransaction: signedTransaction,
amount: amount,
fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction,
exponent: exponent,
);
}
Uint8List hexToBytes(String hexString) {
if (hexString.startsWith('0x')) {
hexString = hexString.substring(2);
}
return Uint8List.fromList(hex.HEX.decode(hexString));
}
class EthereumClient extends EVMChainClient {
@override
int get chainId => 1; int get chainId => 1;
Transaction createTransaction({ @override
required EthereumAddress from,
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
Uint8List? data,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
data: data,
);
}
Future<String> sendTransaction(Uint8List signedTransaction) async =>
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) =>
prependTransactionType(0x02, signedTransaction); prependTransactionType(0x02, signedTransaction);
Future getTransactionDetails(String transactionHash) async { @override
// Wait for the transaction receipt to become available Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
TransactionReceipt? receipt;
while (receipt == null) {
receipt = await _client!.getTransactionReceipt(transactionHash);
await Future.delayed(Duration(seconds: 1));
}
// Print the receipt information
print('Transaction Hash: ${receipt.transactionHash}');
print('Block Hash: ${receipt.blockHash}');
print('Block Number: ${receipt.blockNumber}');
print('Gas Used: ${receipt.gasUsed}');
/*
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 21000
*/
// Wait for the transaction receipt to become available
TransactionInformation? transactionInformation;
while (transactionInformation == null) {
print("********************************");
transactionInformation = await _client!.getTransactionByHash(transactionHash);
await Future.delayed(Duration(seconds: 1));
}
// Print the receipt information
print('Transaction Hash: ${transactionInformation.hash}');
print('Block Hash: ${transactionInformation.blockHash}');
print('Block Number: ${transactionInformation.blockNumber}');
print('Gas Used: ${transactionInformation.gas}');
/*
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 53000
*/
}
Future<ERC20Balance> fetchERC20Balances(
EthereumAddress userAddress, String contractAddress) async {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
try {
final balance = await erc20.balanceOf(userAddress);
int exponent = (await erc20.decimals()).toInt();
return ERC20Balance(balance, exponent: exponent);
} catch (_) {
return ERC20Balance(BigInt.zero);
}
}
Future<Erc20Token?> getErc20Token(String contractAddress) async {
try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final name = await erc20.name();
final symbol = await erc20.symbol();
final decimal = await erc20.decimals();
return Erc20Token(
name: name,
symbol: symbol,
contractAddress: contractAddress,
decimal: decimal.toInt(),
);
} catch (e) {
return null;
}
}
void stop() {
_client?.dispose();
}
Future<List<EthereumTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async { {String? contractAddress}) async {
try { try {
final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", {
@ -246,41 +27,18 @@ I/flutter ( 4474): Gas Used: 53000
"apikey": secrets.etherScanApiKey, "apikey": secrets.etherScanApiKey,
})); }));
final _jsonResponse = json.decode(response.body) as Map<String, dynamic>; final jsonResponse = json.decode(response.body) as Map<String, dynamic>;
if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) { if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (_jsonResponse['result'] as List) return (jsonResponse['result'] as List)
.map((e) => EthereumTransactionModel.fromJson(e as Map<String, dynamic>)) .map((e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'ETH'))
.toList(); .toList();
} }
return []; return [];
} catch (e) { } catch (e) {
print(e); log(e.toString());
return []; return [];
} }
} }
Web3Client? getWeb3Client() {
return _client;
}
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
//
// final contract = DeployedContract(
// contractAbi,
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
// );
// final decimalsFunction = contract.function('decimals');
// final decimals = await _client!.call(
// contract: contract,
// function: decimalsFunction,
// params: [],
// );
//
// int exponent = int.parse(decimals.first.toString());
// return exponent;
// }
} }

View file

@ -1,11 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
class EthereumTransactionCreationException implements Exception {
final String exceptionMessage;
EthereumTransactionCreationException(CryptoCurrency currency) :
this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
@override
String toString() => exceptionMessage;
}

View file

@ -1,25 +0,0 @@
import 'package:intl/intl.dart';
const ethereumAmountLength = 12;
const ethereumAmountDivider = 1000000000000;
final ethereumAmountFormat = NumberFormat()
..maximumFractionDigits = ethereumAmountLength
..minimumFractionDigits = 1;
class EthereumFormatter {
static int parseEthereumAmount(String amount) {
try {
return (double.parse(amount) * ethereumAmountDivider).round();
} catch (_) {
return 0;
}
}
static double parseEthereumAmountToDouble(int amount) {
try {
return amount / ethereumAmountDivider;
} catch (_) {
return 0;
}
}
}

View file

@ -0,0 +1,5 @@
class EthereumMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}

View file

@ -1,77 +1,18 @@
import 'dart:convert';
import 'dart:core'; import 'dart:core';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/file.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_ethereum/ethereum_transaction_info.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart';
part 'ethereum_transaction_history.g.dart'; class EthereumTransactionHistory extends EVMChainTransactionHistory {
EthereumTransactionHistory({
const transactionsHistoryFileName = 'transactions.json'; required super.walletInfo,
required super.password,
class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; });
abstract class EthereumTransactionHistoryBase
extends TransactionHistoryBase<EthereumTransactionInfo> with Store {
EthereumTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, EthereumTransactionInfo>();
}
final WalletInfo walletInfo;
String _password;
Future<void> init() async => await _load();
@override @override
Future<void> save() async { String getTransactionHistoryFileName() => 'transactions.json';
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final data = json.encode({'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch (e, s) {
print('Error while save ethereum transaction history: ${e.toString()}');
print(s);
}
}
@override @override
void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val) =>
EthereumTransactionInfo.fromJson(val);
@override
void addMany(Map<String, EthereumTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password);
if (content.isEmpty) {
return {};
}
return json.decode(content) as Map<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = EthereumTransactionInfo.fromJson(val);
_update(tx);
}
});
} catch (e) {
print(e);
}
}
void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction;
} }

View file

@ -1,57 +1,21 @@
import 'dart:math';
import 'package:cw_core/format_amount.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_evm/evm_chain_transaction_info.dart';
class EthereumTransactionInfo extends TransactionInfo { class EthereumTransactionInfo extends EVMChainTransactionInfo {
EthereumTransactionInfo({ EthereumTransactionInfo({
required this.id, required super.id,
required this.height, required super.height,
required this.ethAmount, required super.ethAmount,
required this.ethFee, required super.ethFee,
this.tokenSymbol = "ETH", required super.tokenSymbol,
this.exponent = 18, required super.direction,
required this.direction, required super.isPending,
required this.isPending, required super.date,
required this.date, required super.confirmations,
required this.confirmations, required super.to,
required this.to, required super.from,
}) : this.amount = ethAmount.toInt(), super.exponent,
this.fee = ethFee.toInt(); });
final String id;
final int height;
final int amount;
final BigInt ethAmount;
final int exponent;
final TransactionDirection direction;
final DateTime date;
final bool isPending;
final int fee;
final BigInt ethFee;
final int confirmations;
final String tokenSymbol;
String? _fiatAmount;
final String? to;
@override
String amountFormatted() {
final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
return '${amount.substring(0, min(10, amount.length))} $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() {
final amount = (ethFee / BigInt.from(10).pow(18)).toString();
return '${amount.substring(0, min(10, amount.length))} ETH';
}
factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) { factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) {
return EthereumTransactionInfo( return EthereumTransactionInfo(
@ -66,20 +30,10 @@ class EthereumTransactionInfo extends TransactionInfo {
confirmations: data['confirmations'] as int, confirmations: data['confirmations'] as int,
tokenSymbol: data['tokenSymbol'] as String, tokenSymbol: data['tokenSymbol'] as String,
to: data['to'], to: data['to'],
from: data['from'],
); );
} }
Map<String, dynamic> toJson() => { @override
'id': id, String get feeCurrency => 'ETH';
'height': height,
'amount': ethAmount.toString(),
'exponent': exponent,
'fee': ethFee.toString(),
'direction': direction.index,
'date': date.millisecondsSinceEpoch,
'isPending': isPending,
'confirmations': confirmations,
'tokenSymbol': tokenSymbol,
'to': to,
};
} }

View file

@ -1,52 +0,0 @@
import 'package:cw_core/transaction_priority.dart';
class EthereumTransactionPriority extends TransactionPriority {
final int tip;
const EthereumTransactionPriority({required String title, required int raw, required this.tip})
: super(title: title, raw: raw);
static const List<EthereumTransactionPriority> all = [fast, medium, slow];
static const EthereumTransactionPriority slow =
EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1);
static const EthereumTransactionPriority medium =
EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2);
static const EthereumTransactionPriority fast =
EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4);
static EthereumTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize');
}
}
String get units => 'gas';
@override
String toString() {
var label = '';
switch (this) {
case EthereumTransactionPriority.slow:
label = 'Slow';
break;
case EthereumTransactionPriority.medium:
label = 'Medium';
break;
case EthereumTransactionPriority.fast:
label = 'Fast';
break;
default:
break;
}
return label;
}
}

View file

@ -1,126 +1,58 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.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_ethereum_erc20_tokens.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.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_formatter.dart';
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
import 'package:cw_ethereum/ethereum_transaction_history.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart';
import 'package:cw_ethereum/ethereum_transaction_info.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart';
import 'package:cw_ethereum/ethereum_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_ethereum/file.dart'; import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:hive/hive.dart'; import 'package:cw_evm/file.dart';
import 'package:hex/hex.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip32/bip32.dart' as bip32;
part 'ethereum_wallet.g.dart'; class EthereumWallet extends EVMChainWallet {
EthereumWallet({
required super.client,
required super.password,
required super.walletInfo,
super.mnemonic,
super.initialBalance,
super.privateKey,
}) : super(nativeCurrency: CryptoCurrency.eth);
class EthereumWallet = EthereumWalletBase with _$EthereumWallet; @override
void addInitialTokens() {
final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens;
abstract class EthereumWalletBase for (var token in initialErc20Tokens) {
extends WalletBase<ERC20Balance, EthereumTransactionHistory, EthereumTransactionInfo> evmChainErc20TokensBox.put(token.contractAddress, token);
with Store {
EthereumWalletBase({
required WalletInfo walletInfo,
String? mnemonic,
String? privateKey,
required String password,
ERC20Balance? initialBalance,
}) : syncStatus = NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_isTransactionUpdating = false,
_client = EthereumClient(),
walletAddresses = EthereumWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
{CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
} }
_sharedPrefs.complete(SharedPreferences.getInstance());
} }
final String? _mnemonic; @override
final String? _hexPrivateKey; Future<bool> checkIfScanProviderIsEnabled() async {
final String _password; bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true;
return isEtherscanEnabled;
late final Box<Erc20Token> erc20TokensBox; }
late final Box<Erc20Token> ethereumErc20TokensBox;
late final EthPrivateKey _ethPrivateKey;
EthPrivateKey get ethPrivateKey => _ethPrivateKey;
late EthereumClient _client;
int? _gasPrice;
int? _estimatedGas;
bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
Timer? _transactionsUpdateTimer;
@override @override
WalletAddresses walletAddresses; Future<void> initErc20TokensBox() async {
// This is for ethereum wallets,
@override // Other wallets would override and initialize their respective boxes with their boxNames.
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
Completer<SharedPreferences> _sharedPrefs = Completer();
Future<void> init() async {
await movePreviousErc20BoxConfigsToNewBox(); await movePreviousErc20BoxConfigsToNewBox();
await walletAddresses.init();
await transactionHistory.init();
_ethPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
password: _password,
);
walletAddresses.address = _ethPrivateKey.address.toString();
await save();
} }
/// Majorly for backward compatibility for previous configs that have been set. /// Majorly for backward compatibility for previous configs that have been set.
Future<void> movePreviousErc20BoxConfigsToNewBox() async { Future<void> movePreviousErc20BoxConfigsToNewBox() async {
// Opens a box specific to this wallet // Opens a box specific to this wallet
ethereumErc20TokensBox = await CakeHive.openBox<Erc20Token>( evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(
"${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}"); "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}");
//Open the previous token configs box //Open the previous token configs box
@ -130,7 +62,7 @@ abstract class EthereumWalletBase
if (erc20TokensBox.isEmpty) { if (erc20TokensBox.isEmpty) {
// If it's empty, but the new wallet specific box is also empty, // If it's empty, but the new wallet specific box is also empty,
// we load the initial tokens to the new box. // we load the initial tokens to the new box.
if (ethereumErc20TokensBox.isEmpty) addInitialTokens(); if (evmChainErc20TokensBox.isEmpty) addInitialTokens();
return; return;
} }
@ -141,327 +73,37 @@ abstract class EthereumWalletBase
await erc20TokensBox.deleteFromDisk(); await erc20TokensBox.deleteFromDisk();
// Add all the previous tokens with configs to the new box // Add all the previous tokens with configs to the new box
ethereumErc20TokensBox.addAll(allValues); evmChainErc20TokensBox.addAll(allValues);
} }
@override @override
int calculateEstimatedFee(TransactionPriority priority, int? amount) { EVMChainTransactionInfo getTransactionInfo(
try { EVMChainTransactionModel transactionModel, String address) {
if (priority is EthereumTransactionPriority) { final model = EthereumTransactionInfo(
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); id: transactionModel.hash,
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); height: transactionModel.blockNumber,
} ethAmount: transactionModel.amount,
direction: transactionModel.from == address
return 0; ? TransactionDirection.outgoing
} catch (e) { : TransactionDirection.incoming,
return 0; isPending: false,
} date: transactionModel.date,
} confirmations: transactionModel.confirmations,
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
@override exponent: transactionModel.tokenDecimal ?? 18,
Future<void> changePassword(String password) { tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
throw UnimplementedError("changePassword"); to: transactionModel.to,
} from: transactionModel.from,
@override
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("Ethereum Node connection failed");
}
_client.setListeners(_ethPrivateKey.address, _onNewTransaction);
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as EthereumTransactionCredentials;
final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final String? opReturnMemo = outputs.first.memo;
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEthereumMultiplier = pow(10, exponent);
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw EthereumTransactionCreationException(transactionCurrency);
}
final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble(
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
if (_erc20Balance.balance < totalAmount) {
throw EthereumTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
// since the fees are taken from Ethereum
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance;
} else {
allAmount = _erc20Balance.balance -
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
}
final totalOriginalAmount =
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEthereumMultiplier);
if (_erc20Balance.balance < totalAmount) {
throw EthereumTransactionCreationException(transactionCurrency);
}
}
String? hexOpReturnMemo;
if (opReturnMemo != null) {
hexOpReturnMemo = '0x' + opReturnMemo.codeUnits.map((char) => char.toRadixString(16).padLeft(2, '0')).join();
}
final pendingEthereumTransaction = await _client.signTransaction(
privateKey: _ethPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount.toString(),
gas: _estimatedGas!,
priority: _credentials.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
data: hexOpReturnMemo,
); );
return model;
return pendingEthereumTransaction;
}
Future<void> _updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true;
if (!isEtherscanEnabled) {
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() async { String getTransactionHistoryFileName() => 'transactions.json';
final address = _ethPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
final List<Future<List<EthereumTransactionModel>>> erc20TokensTransactions = [];
for (var token in balance.keys) {
if (token is Erc20Token) {
erc20TokensTransactions.add(_client.fetchTransactions(
address,
contractAddress: token.contractAddress,
));
}
}
final tokensTransaction = await Future.wait(erc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
final Map<String, EthereumTransactionInfo> result = {};
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
}
result[transactionModel.hash] = EthereumTransactionInfo(
id: transactionModel.hash,
height: transactionModel.blockNumber,
ethAmount: transactionModel.amount,
direction: transactionModel.from == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
isPending: false,
date: transactionModel.date,
confirmations: transactionModel.confirmations,
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
exponent: transactionModel.tokenDecimal ?? 18,
tokenSymbol: transactionModel.tokenSymbol ?? "ETH",
to: transactionModel.to,
);
}
return result;
}
@override @override
Object get keys => throw UnimplementedError("keys"); Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) {
return Erc20Token(
@override
Future<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => HEX.encode(_ethPrivateKey.privateKey);
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic(
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10),
(timer) async => _estimatedGas = await _client.getEstimatedGas());
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
static Future<EthereumWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero);
return EthereumWallet(
walletInfo: walletInfo,
password: password,
mnemonic: mnemonic,
privateKey: privateKey,
initialBalance: balance,
);
}
Future<void> _updateBalance() async {
balance[currency] = await _fetchEthBalance();
await _fetchErc20Balances();
await save();
}
Future<ERC20Balance> _fetchEthBalance() async {
final balance = await _client.getBalance(_ethPrivateKey.address);
return ERC20Balance(balance.getInWei);
}
Future<void> _fetchErc20Balances() async {
for (var token in ethereumErc20TokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchERC20Balances(
_ethPrivateKey.address,
token.contractAddress,
);
} else {
balance.remove(token);
}
} catch (_) {}
}
}
Future<EthPrivateKey> getPrivateKey(
{String? mnemonic, String? privateKey, required String password}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
return EthPrivateKey.fromHex(privateKey);
}
final seed = bip39.mnemonicToSeed(mnemonic!);
final root = bip32.BIP32.fromSeed(seed);
const _hdPathEthereum = "m/44'/60'/0'/0";
const index = 0;
final addressAtIndex = root.derivePath("$_hdPathEthereum/$index");
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
}
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => ethereumErc20TokensBox.values.toList();
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
final _token = Erc20Token(
name: token.name, name: token.name,
symbol: token.symbol, symbol: token.symbol,
contractAddress: token.contractAddress, contractAddress: token.contractAddress,
@ -470,85 +112,30 @@ abstract class EthereumWalletBase
tag: token.tag ?? "ETH", tag: token.tag ?? "ETH",
iconPath: iconPath, iconPath: iconPath,
); );
await ethereumErc20TokensBox.put(_token.contractAddress, _token);
if (_token.enabled) {
balance[_token] = await _client.fetchERC20Balances(
_ethPrivateKey.address,
_token.contractAddress,
);
} else {
balance.remove(_token);
}
}
Future<void> deleteErc20Token(Erc20Token token) async {
await token.delete();
balance.remove(token);
_updateBalance();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
await _client.getErc20Token(contractAddress);
void _onNewTransaction() {
_updateBalance();
_updateTransactions();
}
void addInitialTokens() {
final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens;
initialErc20Tokens.forEach((token) => ethereumErc20TokensBox.put(token.contractAddress, token));
} }
@override @override
Future<void> renameWalletFiles(String newWalletName) async { EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); return EthereumTransactionHistory(walletInfo: walletInfo, password: password);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
} }
void _setTransactionUpdateTimer() { static Future<EthereumWallet> open(
if (_transactionsUpdateTimer?.isActive ?? false) { {required String name, required String password, required WalletInfo walletInfo}) async {
_transactionsUpdateTimer!.cancel(); final path = await pathForWallet(name: name, type: walletInfo.type);
} final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?;
final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
EVMChainERC20Balance(BigInt.zero);
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) { return EthereumWallet(
_updateTransactions(); walletInfo: walletInfo,
_updateBalance(); password: password,
}); mnemonic: mnemonic,
privateKey: privateKey,
initialBalance: balance,
client: EthereumClient(),
);
} }
void updateEtherscanUsageState(bool isEnabled) {
if (isEnabled) {
_updateTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
@override
String signMessage(String message, {String? address}) =>
bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();
} }

View file

@ -1,29 +0,0 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class EthereumNewWalletCredentials extends WalletCredentials {
EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials {
EthereumRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class EthereumRestoreWalletFromPrivateKey extends WalletCredentials {
EthereumRestoreWalletFromPrivateKey(
{required String name,
required String password,
required this.privateKey,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String privateKey;
}

View file

@ -1,32 +1,31 @@
import 'dart:io';
import 'package:cw_core/pathForWallet.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_service.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_ethereum/ethereum_mnemonics.dart'; import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_ethereum/ethereum_mnemonics_exception.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_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:hive/hive.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:collection/collection.dart';
class EthereumWalletService extends WalletService<EthereumNewWalletCredentials, class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
EthereumRestoreWalletFromSeedCredentials, EthereumRestoreWalletFromPrivateKey> { EthereumWalletService(super.walletInfoSource, {required this.client});
EthereumWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource; late EthereumClient client;
@override @override
Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async { WalletType getType() => WalletType.ethereum;
@override
Future<EthereumWallet> create(EVMChainNewWalletCredentials credentials) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = bip39.generateMnemonic(strength: strength); final mnemonic = bip39.generateMnemonic(strength: strength);
final wallet = EthereumWallet( final wallet = EthereumWallet(
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
mnemonic: mnemonic, mnemonic: mnemonic,
password: credentials.password!, password: credentials.password!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -36,18 +35,11 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
return wallet; return wallet;
} }
@override
WalletType getType() => WalletType.ethereum;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override @override
Future<EthereumWallet> openWallet(String name, String password) async { Future<EthereumWallet> openWallet(String name, String password) async {
final walletInfo = final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await EthereumWalletBase.open( final wallet = await EthereumWallet.open(
name: name, name: name,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
@ -60,19 +52,28 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
} }
@override @override
Future<void> remove(String wallet) async { Future<void> rename(String currentName, String password, String newName) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); final currentWalletInfo = walletInfoSource.values
final walletInfo = walletInfoSource.values .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; final currentWallet = await EthereumWallet.open(
await walletInfoSource.delete(walletInfo.key); password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override @override
Future<EthereumWallet> restoreFromKeys(EthereumRestoreWalletFromPrivateKey credentials) async { Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
final wallet = EthereumWallet( final wallet = EthereumWallet(
password: credentials.password!, password: credentials.password!,
privateKey: credentials.privateKey, privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -84,7 +85,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
@override @override
Future<EthereumWallet> restoreFromSeed( Future<EthereumWallet> restoreFromSeed(
EthereumRestoreWalletFromSeedCredentials credentials) async { EVMChainRestoreWalletFromSeedCredentials credentials) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) { if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw EthereumMnemonicIsIncorrectException(); throw EthereumMnemonicIsIncorrectException();
} }
@ -93,6 +94,7 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
password: credentials.password!, password: credentials.password!,
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -101,20 +103,4 @@ class EthereumWalletService extends WalletService<EthereumNewWalletCredentials,
return wallet; return wallet;
} }
@override
Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = await EthereumWalletBase.open(
password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
}
} }

View file

@ -13,56 +13,23 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
web3dart: ^2.7.1 web3dart: ^2.7.1
erc20: ^1.0.1
mobx: ^2.0.7+4
bip39: ^1.0.6
bip32: ^2.0.0
hex: ^0.2.0
http: ^1.1.0
shared_preferences: ^2.0.15
cw_core: cw_core:
path: ../cw_core path: ../cw_core
cw_evm:
path: ../cw_evm
hive: ^2.2.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.1.11
mobx_codegen: ^2.0.7
hive_generator: ^1.1.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter: flutter:
# To add assets to your package, add an assets section, like this:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts: # fonts:
# - family: Schyler # - family: Schyler
# fonts: # fonts:
# - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf # - asset: fonts/Schyler-Italic.ttf
# style: italic # style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages

30
cw_evm/.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/

10
cw_evm/.metadata Normal file
View file

@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable
project_type: package

3
cw_evm/CHANGELOG.md Normal file
View file

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
cw_evm/LICENSE Normal file
View file

@ -0,0 +1 @@
TODO: Add your license here.

39
cw_evm/README.md Normal file
View file

@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

View file

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

7
cw_evm/lib/cw_evm.dart Normal file
View file

@ -0,0 +1,7 @@
library cw_evm;
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}

View file

@ -0,0 +1,251 @@
import 'dart:async';
import 'dart:developer';
import 'package:cw_core/node.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:erc20/erc20.dart';
import 'package:web3dart/web3dart.dart';
abstract class EVMChainClient {
final httpClient = Client();
Web3Client? _client;
//! To be overridden by all child classes
int get chainId;
Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress});
Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction);
//! Common methods across all child classes
bool connect(Node node) {
try {
_client = Web3Client(node.uri.toString(), httpClient);
return true;
} catch (e) {
return false;
}
}
void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async {
// _client?.pendingTransactions().listen((transactionHash) async {
// final transaction = await _client!.getTransactionByHash(transactionHash);
//
// if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) {
// onNewTransaction();
// }
// });
}
Future<EtherAmount> getBalance(EthereumAddress address) async {
try {
return await _client!.getBalance(address);
} catch (_) {
return EtherAmount.zero();
}
}
Future<int> getGasUnitPrice() async {
try {
final gasPrice = await _client!.getGasPrice();
return gasPrice.getInWei.toInt();
} catch (_) {
return 0;
}
}
Future<int> getEstimatedGas() async {
try {
final estimatedGas = await _client!.estimateGas();
return estimatedGas.toInt();
} catch (_) {
return 0;
}
}
Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey,
required String toAddress,
required String amount,
required int gas,
required EVMChainTransactionPriority priority,
required CryptoCurrency currency,
required int exponent,
String? contractAddress,
}) async {
assert(currency == CryptoCurrency.eth ||
currency == CryptoCurrency.maticpoly ||
contractAddress != null);
bool isEVMCompatibleChain =
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice();
final Transaction transaction = createTransaction(
from: privateKey.address,
to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(),
);
final signedTransaction =
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
final Function _sendTransaction;
if (isEVMCompatibleChain) {
_sendTransaction = () async => await sendTransaction(signedTransaction);
} else {
final erc20 = ERC20(
client: _client!,
address: EthereumAddress.fromHex(contractAddress!),
chainId: chainId,
);
_sendTransaction = () async {
await erc20.transfer(
EthereumAddress.fromHex(toAddress),
BigInt.parse(amount),
credentials: privateKey,
transaction: transaction,
);
};
}
return PendingEVMChainTransaction(
signedTransaction: signedTransaction,
amount: amount,
fee: BigInt.from(gas) * (await price).getInWei,
sendTransaction: _sendTransaction,
exponent: exponent,
);
}
Transaction createTransaction({
required EthereumAddress from,
required EthereumAddress to,
required EtherAmount amount,
EtherAmount? maxPriorityFeePerGas,
}) {
return Transaction(
from: from,
to: to,
maxPriorityFeePerGas: maxPriorityFeePerGas,
value: amount,
);
}
Future<String> sendTransaction(Uint8List signedTransaction) async =>
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
Future getTransactionDetails(String transactionHash) async {
// Wait for the transaction receipt to become available
TransactionReceipt? receipt;
while (receipt == null) {
receipt = await _client!.getTransactionReceipt(transactionHash);
await Future.delayed(const Duration(seconds: 1));
}
// Print the receipt information
log('Transaction Hash: ${receipt.transactionHash}');
log('Block Hash: ${receipt.blockHash}');
log('Block Number: ${receipt.blockNumber}');
log('Gas Used: ${receipt.gasUsed}');
/*
Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116]
I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200]
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 21000
*/
// Wait for the transaction receipt to become available
TransactionInformation? transactionInformation;
while (transactionInformation == null) {
log("********************************");
transactionInformation = await _client!.getTransactionByHash(transactionHash);
await Future.delayed(const Duration(seconds: 1));
}
// Print the receipt information
log('Transaction Hash: ${transactionInformation.hash}');
log('Block Hash: ${transactionInformation.blockHash}');
log('Block Number: ${transactionInformation.blockNumber}');
log('Gas Used: ${transactionInformation.gas}');
/*
Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74
I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8
I/flutter ( 4474): Block Number: 17120242
I/flutter ( 4474): Gas Used: 53000
*/
}
Future<EVMChainERC20Balance> fetchERC20Balances(
EthereumAddress userAddress, String contractAddress) async {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final balance = await erc20.balanceOf(userAddress);
int exponent = (await erc20.decimals()).toInt();
return EVMChainERC20Balance(balance, exponent: exponent);
}
Future<Erc20Token?> getErc20Token(String contractAddress) async {
try {
final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!);
final name = await erc20.name();
final symbol = await erc20.symbol();
final decimal = await erc20.decimals();
return Erc20Token(
name: name,
symbol: symbol,
contractAddress: contractAddress,
decimal: decimal.toInt(),
);
} catch (e) {
return null;
}
}
void stop() {
_client?.dispose();
}
Web3Client? getWeb3Client() {
return _client;
}
// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async {
// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json");
// final contractAbi = ContractAbi.fromJson(abi, "ERC20");
//
// final contract = DeployedContract(
// contractAbi,
// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!),
// );
// final decimalsFunction = contract.function('decimals');
// final decimals = await _client!.call(
// contract: contract,
// function: decimalsFunction,
// params: [],
// );
//
// int exponent = int.parse(decimals.first.toString());
// return exponent;
// }
}

View file

@ -0,0 +1,11 @@
import 'package:cw_core/crypto_currency.dart';
class EVMChainTransactionCreationException implements Exception {
final String exceptionMessage;
EVMChainTransactionCreationException(CryptoCurrency currency)
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
@override
String toString() => exceptionMessage;
}

View file

@ -0,0 +1,25 @@
import 'package:intl/intl.dart';
const evmChainAmountLength = 12;
const evmChainAmountDivider = 1000000000000;
final evmChainAmountFormat = NumberFormat()
..maximumFractionDigits = evmChainAmountLength
..minimumFractionDigits = 1;
class EVMChainFormatter {
static int parseEVMChainAmount(String amount) {
try {
return (double.parse(amount) * evmChainAmountDivider).round();
} catch (_) {
return 0;
}
}
static double parseEVMChainAmountToDouble(int amount) {
try {
return amount / evmChainAmountDivider;
} catch (_) {
return 0;
}
}
}

View file

@ -1,10 +1,4 @@
class EthereumMnemonicIsIncorrectException implements Exception { class EVMChainMnemonics {
@override
String toString() =>
'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}
class EthereumMnemonics {
static const englishWordlist = <String>[ static const englishWordlist = <String>[
'abandon', 'abandon',
'ability', 'ability',

View file

@ -1,9 +1,9 @@
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
class EthereumTransactionCredentials { class EVMChainTransactionCredentials {
EthereumTransactionCredentials( EVMChainTransactionCredentials(
this.outputs, { this.outputs, {
required this.priority, required this.priority,
required this.currency, required this.currency,
@ -11,7 +11,7 @@ class EthereumTransactionCredentials {
}); });
final List<OutputInfo> outputs; final List<OutputInfo> outputs;
final EthereumTransactionPriority? priority; final EVMChainTransactionPriority? priority;
final int? feeRate; final int? feeRate;
final CryptoCurrency currency; final CryptoCurrency currency;
} }

View file

@ -0,0 +1,88 @@
import 'dart:convert';
import 'dart:core';
import 'dart:developer';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/file.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
part 'evm_chain_transaction_history.g.dart';
abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase
with _$EVMChainTransactionHistory;
abstract class EVMChainTransactionHistoryBase
extends TransactionHistoryBase<EVMChainTransactionInfo> with Store {
EVMChainTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, EVMChainTransactionInfo>();
}
String _password;
final WalletInfo walletInfo;
//! Method to be overridden by all child classes
String getTransactionHistoryFileName();
EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val);
//! Common methods across all child classes
Future<void> init() async => await _load();
@override
Future<void> save() async {
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final data = json.encode({'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch (e, s) {
log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}');
log(s.toString());
}
}
@override
void addOne(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction;
@override
void addMany(Map<String, EVMChainTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName();
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
String path = '$dirPath/$transactionsHistoryFileNameForWallet';
final content = await read(path: path, password: _password);
if (content.isEmpty) {
return {};
}
return json.decode(content) as Map<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
for (var entry in txs.entries) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = getTransactionInfo(val);
_update(tx);
}
}
} catch (e) {
log(e.toString());
}
}
void _update(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View file

@ -0,0 +1,77 @@
// ignore_for_file: overridden_fields, annotate_overrides
import 'dart:math';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
abstract class EVMChainTransactionInfo extends TransactionInfo {
EVMChainTransactionInfo({
required this.id,
required this.height,
required this.ethAmount,
required this.ethFee,
required this.tokenSymbol,
this.exponent = 18,
required this.direction,
required this.isPending,
required this.date,
required this.confirmations,
required this.to,
required this.from,
}) : amount = ethAmount.toInt(),
fee = ethFee.toInt();
final String id;
final int height;
final int amount;
final BigInt ethAmount;
final int exponent;
final TransactionDirection direction;
final DateTime date;
final bool isPending;
final int fee;
final BigInt ethFee;
final int confirmations;
final String tokenSymbol;
String? _fiatAmount;
final String? to;
final String? from;
//! Getter to be overridden in child classes
String get feeCurrency;
@override
String amountFormatted() {
final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString());
return '${amount.substring(0, min(10, amount.length))} $tokenSymbol';
}
@override
String fiatAmount() => _fiatAmount ?? '';
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() {
final amount = (ethFee / BigInt.from(10).pow(18)).toString();
return '${amount.substring(0, min(10, amount.length))} $feeCurrency';
}
Map<String, dynamic> toJson() => {
'id': id,
'height': height,
'amount': ethAmount.toString(),
'exponent': exponent,
'fee': ethFee.toString(),
'direction': direction.index,
'date': date.millisecondsSinceEpoch,
'isPending': isPending,
'confirmations': confirmations,
'tokenSymbol': tokenSymbol,
'to': to,
'from': from,
};
}

View file

@ -1,5 +1,4 @@
//! Model used for in parsing transactions fetched using etherscan class EVMChainTransactionModel {
class EthereumTransactionModel {
final DateTime date; final DateTime date;
final String hash; final String hash;
final String from; final String from;
@ -14,7 +13,7 @@ class EthereumTransactionModel {
final int? tokenDecimal; final int? tokenDecimal;
final bool isError; final bool isError;
EthereumTransactionModel({ EVMChainTransactionModel({
required this.date, required this.date,
required this.hash, required this.hash,
required this.from, required this.from,
@ -30,7 +29,8 @@ class EthereumTransactionModel {
required this.isError, required this.isError,
}); });
factory EthereumTransactionModel.fromJson(Map<String, dynamic> json) => EthereumTransactionModel( factory EVMChainTransactionModel.fromJson(Map<String, dynamic> json, String defaultSymbol) =>
EVMChainTransactionModel(
date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000),
hash: json["hash"], hash: json["hash"],
from: json["from"], from: json["from"],
@ -41,7 +41,7 @@ class EthereumTransactionModel {
contractAddress: json["contractAddress"], contractAddress: json["contractAddress"],
confirmations: int.parse(json["confirmations"]), confirmations: int.parse(json["confirmations"]),
blockNumber: int.parse(json["blockNumber"]), blockNumber: int.parse(json["blockNumber"]),
tokenSymbol: json["tokenSymbol"] ?? "ETH", tokenSymbol: json["tokenSymbol"] ?? defaultSymbol,
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1", isError: json["isError"] == "1",
); );

View file

@ -0,0 +1,52 @@
import 'package:cw_core/transaction_priority.dart';
class EVMChainTransactionPriority extends TransactionPriority {
final int tip;
const EVMChainTransactionPriority({required String title, required int raw, required this.tip})
: super(title: title, raw: raw);
static const List<EVMChainTransactionPriority> all = [fast, medium, slow];
static const EVMChainTransactionPriority slow =
EVMChainTransactionPriority(title: 'slow', raw: 0, tip: 1);
static const EVMChainTransactionPriority medium =
EVMChainTransactionPriority(title: 'Medium', raw: 1, tip: 2);
static const EVMChainTransactionPriority fast =
EVMChainTransactionPriority(title: 'Fast', raw: 2, tip: 4);
static EVMChainTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
throw Exception('Unexpected token: $raw for EVMChainTransactionPriority deserialize');
}
}
String get units => 'gas';
@override
String toString() {
var label = '';
switch (this) {
case EVMChainTransactionPriority.slow:
label = 'Slow';
break;
case EVMChainTransactionPriority.medium:
label = 'Medium';
break;
case EVMChainTransactionPriority.fast:
label = 'Fast';
break;
default:
break;
}
return label;
}
}

View file

@ -0,0 +1,512 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_evm/evm_chain_client.dart';
import 'package:cw_evm/evm_chain_exceptions.dart';
import 'package:cw_evm/evm_chain_formatter.dart';
import 'package:cw_evm/evm_chain_transaction_credentials.dart';
import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_wallet_addresses.dart';
import 'package:cw_evm/file.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'evm_chain_transaction_info.dart';
import 'evm_erc20_balance.dart';
part 'evm_chain_wallet.g.dart';
abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet;
abstract class EVMChainWalletBase
extends WalletBase<EVMChainERC20Balance, EVMChainTransactionHistory, EVMChainTransactionInfo>
with Store {
EVMChainWalletBase({
required WalletInfo walletInfo,
required EVMChainClient client,
required CryptoCurrency nativeCurrency,
String? mnemonic,
String? privateKey,
required String password,
EVMChainERC20Balance? initialBalance,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_isTransactionUpdating = false,
_client = client,
walletAddresses = EVMChainWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, EVMChainERC20Balance>.of(
{
// Not sure of this yet, will it work? will it not?
nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero),
},
),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = setUpTransactionHistory(walletInfo, password);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
}
sharedPrefs.complete(SharedPreferences.getInstance());
}
final String? _mnemonic;
final String? _hexPrivateKey;
final String _password;
late final Box<Erc20Token> erc20TokensBox;
late final Box<Erc20Token> evmChainErc20TokensBox;
late final EthPrivateKey _evmChainPrivateKey;
EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey;
late EVMChainClient _client;
int? _gasPrice;
int? _estimatedGas;
bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
Timer? _transactionsUpdateTimer;
@override
WalletAddresses walletAddresses;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, EVMChainERC20Balance> balance;
Completer<SharedPreferences> sharedPrefs = Completer();
//! Methods to be overridden by every child
void addInitialTokens();
// Future<EVMChainWallet> open({
// required String name,
// required String password,
// required WalletInfo walletInfo,
// });
Future<void> initErc20TokensBox();
String getTransactionHistoryFileName();
Future<bool> checkIfScanProviderIsEnabled();
EVMChainTransactionInfo getTransactionInfo(
EVMChainTransactionModel transactionModel, String address);
Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath);
EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password);
//! Common Methods across child classes
String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
Future<void> init() async {
await initErc20TokensBox();
await walletAddresses.init();
await transactionHistory.init();
_evmChainPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
password: _password,
);
walletAddresses.address = _evmChainPrivateKey.address.toString();
await save();
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
try {
if (priority is EVMChainTransactionPriority) {
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt();
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0);
}
return 0;
} catch (e) {
return 0;
}
}
@override
Future<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
@override
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
}
_client.setListeners(_evmChainPrivateKey.address, _onNewTransaction);
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic(
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10),
(timer) async => _estimatedGas = await _client.getEstimatedGas());
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final _credentials = credentials as EVMChainTransactionCredentials;
final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == _credentials.currency.title);
final _erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToEVMChainMultiplier = pow(10, exponent);
// so far this can not be made with Ethereum as Ethereum does not support multiple recipients
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble(
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
// since the fees are taken from Ethereum
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = _erc20Balance.balance;
} else {
allAmount = _erc20Balance.balance -
BigInt.from(calculateEstimatedFee(_credentials.priority!, null));
}
final totalOriginalAmount =
EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount = output.sendAll
? allAmount
: BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier);
if (_erc20Balance.balance < totalAmount) {
throw EVMChainTransactionCreationException(transactionCurrency);
}
}
final pendingEVMChainTransaction = await _client.signTransaction(
privateKey: _evmChainPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress
? _credentials.outputs.first.extractedAddress!
: _credentials.outputs.first.address,
amount: totalAmount.toString(),
gas: _estimatedGas!,
priority: _credentials.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
);
return pendingEVMChainTransaction;
}
Future<void> _updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
final isProviderEnabled = await checkIfScanProviderIsEnabled();
if (!isProviderEnabled) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (_) {
_isTransactionUpdating = false;
}
}
@override
Future<Map<String, EVMChainTransactionInfo>> fetchTransactions() async {
final address = _evmChainPrivateKey.address.hex;
final transactions = await _client.fetchTransactions(address);
final List<Future<List<EVMChainTransactionModel>>> erc20TokensTransactions = [];
for (var token in balance.keys) {
if (token is Erc20Token) {
erc20TokensTransactions.add(_client.fetchTransactions(
address,
contractAddress: token.contractAddress,
));
}
}
final tokensTransaction = await Future.wait(erc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
final Map<String, EVMChainTransactionInfo> result = {};
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
}
result[transactionModel.hash] = getTransactionInfo(transactionModel, address);
}
return result;
}
@override
Object get keys => throw UnimplementedError("keys");
@override
Future<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => HEX.encode(_evmChainPrivateKey.privateKey);
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
Future<void> _updateBalance() async {
balance[currency] = await _fetchEVMChainBalance();
await _fetchErc20Balances();
await save();
}
Future<EVMChainERC20Balance> _fetchEVMChainBalance() async {
final balance = await _client.getBalance(_evmChainPrivateKey.address);
return EVMChainERC20Balance(balance.getInWei);
}
Future<void> _fetchErc20Balances() async {
for (var token in evmChainErc20TokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchERC20Balances(
_evmChainPrivateKey.address,
token.contractAddress,
);
} else {
balance.remove(token);
}
} catch (_) {}
}
}
Future<EthPrivateKey> getPrivateKey(
{String? mnemonic, String? privateKey, required String password}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
return EthPrivateKey.fromHex(privateKey);
}
final seed = bip39.mnemonicToSeed(mnemonic!);
final root = bip32.BIP32.fromSeed(seed);
const hdPathEVMChain = "m/44'/60'/0'/0";
const index = 0;
final addressAtIndex = root.derivePath("$hdPathEVMChain/$index");
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
}
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => evmChainErc20TokensBox.values.toList();
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
final newToken = createNewErc20TokenObject(token, iconPath);
await evmChainErc20TokensBox.put(newToken.contractAddress, newToken);
if (newToken.enabled) {
balance[newToken] = await _client.fetchERC20Balances(
_evmChainPrivateKey.address,
newToken.contractAddress,
);
} else {
balance.remove(newToken);
}
}
Future<void> deleteErc20Token(Erc20Token token) async {
await token.delete();
balance.remove(token);
_updateBalance();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
await _client.getErc20Token(contractAddress);
void _onNewTransaction() {
_updateBalance();
_updateTransactions();
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final transactionHistoryFileNameForWallet = getTransactionHistoryFileName();
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
}
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_updateTransactions();
_updateBalance();
});
}
/// Scan Providers:
///
/// EtherScan for Ethereum.
///
/// PolygonScan for Polygon.
void updateScanProviderUsageState(bool isEnabled) {
if (isEnabled) {
_updateTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
@override
String signMessage(String message, {String? address}) =>
bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();
}

View file

@ -1,13 +1,15 @@
import 'dart:developer';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'ethereum_wallet_addresses.g.dart'; part 'evm_chain_wallet_addresses.g.dart';
class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses;
abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
EthereumWalletAddressesBase(WalletInfo walletInfo) EVMChainWalletAddressesBase(WalletInfo walletInfo)
: address = '', : address = '',
super(walletInfo); super(walletInfo);
@ -27,7 +29,7 @@ abstract class EthereumWalletAddressesBase extends WalletAddresses with Store {
addressesMap[address] = ''; addressesMap[address] = '';
await saveAddressesInBox(); await saveAddressesInBox();
} catch (e) { } catch (e) {
print(e.toString()); log(e.toString());
} }
} }
} }

View file

@ -0,0 +1,29 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class EVMChainNewWalletCredentials extends WalletCredentials {
EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials {
EVMChainRestoreWalletFromSeedCredentials({
required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials {
EVMChainRestoreWalletFromPrivateKey({
required String name,
required String password,
required this.privateKey,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String privateKey;
}

View file

@ -0,0 +1,50 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:hive/hive.dart';
abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletService<
EVMChainNewWalletCredentials,
EVMChainRestoreWalletFromSeedCredentials,
EVMChainRestoreWalletFromPrivateKey> {
EVMChainWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource;
@override
WalletType getType();
@override
Future<T> create(EVMChainNewWalletCredentials credentials);
@override
Future<T> openWallet(String name, String password);
@override
Future<void> rename(String currentName, String password, String newName);
@override
Future<T> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials);
@override
Future<T> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials);
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}
}

View file

@ -3,10 +3,9 @@ import 'dart:math';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
class ERC20Balance extends Balance { class EVMChainERC20Balance extends Balance {
ERC20Balance(this.balance, {this.exponent = 18}) EVMChainERC20Balance(this.balance, {this.exponent = 18})
: super(balance.toInt(), : super(balance.toInt(), balance.toInt());
balance.toInt());
final BigInt balance; final BigInt balance;
final int exponent; final int exponent;
@ -28,7 +27,7 @@ class ERC20Balance extends Balance {
'exponent': exponent, 'exponent': exponent,
}); });
static ERC20Balance? fromJSON(String? jsonSource) { static EVMChainERC20Balance? fromJSON(String? jsonSource) {
if (jsonSource == null) { if (jsonSource == null) {
return null; return null;
} }
@ -36,12 +35,12 @@ class ERC20Balance extends Balance {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
try { try {
return ERC20Balance( return EVMChainERC20Balance(
BigInt.parse(decoded['balanceInWei']), BigInt.parse(decoded['balanceInWei']),
exponent: decoded['exponent'], exponent: decoded['exponent'],
); );
} catch (e) { } catch (e) {
return ERC20Balance(BigInt.zero); return EVMChainERC20Balance(BigInt.zero);
} }
} }
} }

View file

@ -4,14 +4,14 @@ import 'dart:typed_data';
import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/pending_transaction.dart';
import 'package:web3dart/crypto.dart'; import 'package:web3dart/crypto.dart';
class PendingEthereumTransaction with PendingTransaction { class PendingEVMChainTransaction with PendingTransaction {
final Function sendTransaction; final Function sendTransaction;
final Uint8List signedTransaction; final Uint8List signedTransaction;
final BigInt fee; final BigInt fee;
final String amount; final String amount;
final int exponent; final int exponent;
PendingEthereumTransaction({ PendingEVMChainTransaction({
required this.sendTransaction, required this.sendTransaction,
required this.signedTransaction, required this.signedTransaction,
required this.fee, required this.fee,

45
cw_evm/pubspec.yaml Normal file
View file

@ -0,0 +1,45 @@
name: cw_evm
description: A new Flutter package project.
version: 0.0.1
publish_to: none
author: Cake Wallet
homepage: https://cakewallet.com
environment:
sdk: '>=3.0.6 <4.0.0'
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
web3dart: ^2.7.1
erc20: ^1.0.1
bip39: ^1.0.6
bip32: ^2.0.0
hex: ^0.2.0
http: ^1.1.0
hive: ^2.2.3
collection: ^1.17.1
shared_preferences: ^2.0.15
cw_core:
path: ../cw_core
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.1.11
mobx_codegen: ^2.0.7
hive_generator: ^1.1.3
flutter_lints: ^2.0.0
flutter:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic

View file

@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:cw_evm/cw_evm.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
});
}

View file

@ -1,19 +0,0 @@
import 'dart:typed_data';
import 'package:cw_ethereum/pending_ethereum_transaction.dart';
class PendingPolygonTransaction extends PendingEthereumTransaction {
PendingPolygonTransaction({
required Function sendTransaction,
required Uint8List signedTransaction,
required BigInt fee,
required String amount,
required int exponent,
}) : super(
amount: amount,
sendTransaction: sendTransaction,
signedTransaction: signedTransaction,
fee: fee,
exponent: exponent,
);
}

View file

@ -1,12 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_evm/evm_chain_client.dart';
import 'package:cw_polygon/polygon_transaction_model.dart'; import 'package:cw_evm/.secrets.g.dart' as secrets;
import 'package:cw_ethereum/.secrets.g.dart' as secrets; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
class PolygonClient extends EthereumClient { class PolygonClient extends EVMChainClient {
@override @override
Transaction createTransaction({ Transaction createTransaction({
required EthereumAddress from, required EthereumAddress from,
@ -30,7 +30,7 @@ class PolygonClient extends EthereumClient {
int get chainId => 137; int get chainId => 137;
@override @override
Future<List<PolygonTransactionModel>> fetchTransactions(String address, Future<List<EVMChainTransactionModel>> fetchTransactions(String address,
{String? contractAddress}) async { {String? contractAddress}) async {
try { try {
final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", { final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", {
@ -45,7 +45,9 @@ class PolygonClient extends EthereumClient {
if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) {
return (jsonResponse['result'] as List) return (jsonResponse['result'] as List)
.map((e) => PolygonTransactionModel.fromJson(e as Map<String, dynamic>)) .map(
(e) => EVMChainTransactionModel.fromJson(e as Map<String, dynamic>, 'MATIC'),
)
.toList(); .toList();
} }

View file

@ -1,6 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_ethereum/ethereum_exceptions.dart';
class PolygonTransactionCreationException extends EthereumTransactionCreationException {
PolygonTransactionCreationException(CryptoCurrency currency) : super(currency);
}

View file

@ -1,25 +0,0 @@
import 'package:intl/intl.dart';
const polygonAmountLength = 12;
const polygonAmountDivider = 1000000000000;
final polygonAmountFormat = NumberFormat()
..maximumFractionDigits = polygonAmountLength
..minimumFractionDigits = 1;
class PolygonFormatter {
static int parsePolygonAmount(String amount) {
try {
return (double.parse(amount) * polygonAmountDivider).round();
} catch (_) {
return 0;
}
}
static double parsePolygonAmountToDouble(int amount) {
try {
return amount / polygonAmountDivider;
} catch (_) {
return 0;
}
}
}

View file

@ -1,18 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_ethereum/ethereum_transaction_credentials.dart';
import 'package:cw_polygon/polygon_transaction_priority.dart';
class PolygonTransactionCredentials extends EthereumTransactionCredentials {
PolygonTransactionCredentials(
List<OutputInfo> outputs, {
required PolygonTransactionPriority? priority,
required CryptoCurrency currency,
final int? feeRate,
}) : super(
outputs,
currency: currency,
priority: priority,
feeRate: feeRate,
);
}

View file

@ -1,77 +1,19 @@
import 'dart:convert';
import 'dart:core'; import 'dart:core';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_ethereum/file.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_polygon/polygon_transaction_info.dart'; import 'package:cw_polygon/polygon_transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/transaction_history.dart';
part 'polygon_transaction_history.g.dart'; class PolygonTransactionHistory extends EVMChainTransactionHistory {
PolygonTransactionHistory({
const transactionsHistoryFileName = 'polygon_transactions.json'; required super.walletInfo,
required super.password,
class PolygonTransactionHistory = PolygonTransactionHistoryBase with _$PolygonTransactionHistory; });
abstract class PolygonTransactionHistoryBase extends TransactionHistoryBase<PolygonTransactionInfo>
with Store {
PolygonTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, PolygonTransactionInfo>();
}
final WalletInfo walletInfo;
String _password;
Future<void> init() async => await _load();
@override @override
Future<void> save() async { String getTransactionHistoryFileName() => 'polygon_transactions.json';
try {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final data = json.encode({'transactions': transactions});
await writeData(path: path, password: _password, data: data);
} catch (e, s) {
print('Error while saving polygon transaction history: ${e.toString()}');
print(s);
}
}
@override @override
void addOne(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction; EVMChainTransactionInfo getTransactionInfo(Map<String, dynamic> val) =>
PolygonTransactionInfo.fromJson(val);
@override
void addMany(Map<String, PolygonTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _read() async {
final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type);
final path = '$dirPath/$transactionsHistoryFileName';
final content = await read(path: path, password: _password);
if (content.isEmpty) {
return {};
}
return json.decode(content) as Map<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
txs.entries.forEach((entry) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = PolygonTransactionInfo.fromJson(val);
_update(tx);
}
});
} catch (e) {
print(e);
}
}
void _update(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction;
} }

View file

@ -1,32 +1,21 @@
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_ethereum/ethereum_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart';
class PolygonTransactionInfo extends EthereumTransactionInfo { class PolygonTransactionInfo extends EVMChainTransactionInfo {
PolygonTransactionInfo({ PolygonTransactionInfo({
required String id, required super.id,
required int height, required super.height,
required BigInt ethAmount, required super.ethAmount,
int exponent = 18, required super.ethFee,
required TransactionDirection direction, required super.tokenSymbol,
required DateTime date, required super.direction,
required bool isPending, required super.isPending,
required BigInt ethFee, required super.date,
required int confirmations, required super.confirmations,
String tokenSymbol = "MATIC", required super.to,
required String? to, required super.from,
}) : super( super.exponent,
confirmations: confirmations, });
id: id,
height: height,
ethAmount: ethAmount,
exponent: exponent,
direction: direction,
date: date,
isPending: isPending,
ethFee: ethFee,
to: to,
tokenSymbol: tokenSymbol,
);
factory PolygonTransactionInfo.fromJson(Map<String, dynamic> data) { factory PolygonTransactionInfo.fromJson(Map<String, dynamic> data) {
return PolygonTransactionInfo( return PolygonTransactionInfo(
@ -41,9 +30,10 @@ class PolygonTransactionInfo extends EthereumTransactionInfo {
confirmations: data['confirmations'] as int, confirmations: data['confirmations'] as int,
tokenSymbol: data['tokenSymbol'] as String, tokenSymbol: data['tokenSymbol'] as String,
to: data['to'], to: data['to'],
from: data['from'],
); );
} }
@override @override
String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} MATIC'; String get feeCurrency => 'MATIC';
} }

View file

@ -1,49 +0,0 @@
import 'package:cw_ethereum/ethereum_transaction_model.dart';
class PolygonTransactionModel extends EthereumTransactionModel {
PolygonTransactionModel({
required DateTime date,
required String hash,
required String from,
required String to,
required BigInt amount,
required int gasUsed,
required BigInt gasPrice,
required String contractAddress,
required int confirmations,
required int blockNumber,
required String? tokenSymbol,
required int? tokenDecimal,
required bool isError,
}) : super(
amount: amount,
date: date,
hash: hash,
from: from,
to: to,
gasPrice: gasPrice,
gasUsed: gasUsed,
confirmations: confirmations,
contractAddress: contractAddress,
blockNumber: blockNumber,
tokenDecimal: tokenDecimal,
tokenSymbol: tokenSymbol,
isError: isError,
);
factory PolygonTransactionModel.fromJson(Map<String, dynamic> json) => PolygonTransactionModel(
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"] ?? "MATIC",
tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""),
isError: json["isError"] == "1",
);
}

View file

@ -1,51 +0,0 @@
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
class PolygonTransactionPriority extends EthereumTransactionPriority {
const PolygonTransactionPriority({required String title, required int raw, required int tip})
: super(title: title, raw: raw, tip: tip);
static const List<PolygonTransactionPriority> all = [fast, medium, slow];
static const PolygonTransactionPriority slow =
PolygonTransactionPriority(title: 'slow', raw: 0, tip: 1);
static const PolygonTransactionPriority medium =
PolygonTransactionPriority(title: 'Medium', raw: 1, tip: 2);
static const PolygonTransactionPriority fast =
PolygonTransactionPriority(title: 'Fast', raw: 2, tip: 4);
static PolygonTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
throw Exception('Unexpected token: $raw for PolygonTransactionPriority deserialize');
}
}
@override
String get units => 'gas';
@override
String toString() {
var label = '';
switch (this) {
case PolygonTransactionPriority.slow:
label = 'Slow';
break;
case PolygonTransactionPriority.medium:
label = 'Medium';
break;
case PolygonTransactionPriority.fast:
label = 'Fast';
break;
default:
break;
}
return label;
}
}

View file

@ -1,366 +1,109 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.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/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_ethereum/erc20_balance.dart';
import 'package:cw_ethereum/ethereum_formatter.dart';
import 'package:cw_ethereum/file.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_wallet.dart';
import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/file.dart';
import 'package:cw_polygon/default_polygon_erc20_tokens.dart'; import 'package:cw_polygon/default_polygon_erc20_tokens.dart';
import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_exceptions.dart';
import 'package:cw_polygon/polygon_formatter.dart';
import 'package:cw_polygon/polygon_transaction_credentials.dart';
import 'package:cw_polygon/polygon_transaction_history.dart';
import 'package:cw_polygon/polygon_transaction_info.dart'; import 'package:cw_polygon/polygon_transaction_info.dart';
import 'package:cw_polygon/polygon_transaction_model.dart'; import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_transaction_priority.dart'; import 'package:cw_polygon/polygon_transaction_history.dart';
import 'package:cw_polygon/polygon_wallet_addresses.dart';
import 'package:hive/hive.dart';
import 'package:hex/hex.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip32/bip32.dart' as bip32;
part 'polygon_wallet.g.dart'; class PolygonWallet extends EVMChainWallet {
PolygonWallet({
class PolygonWallet = PolygonWalletBase with _$PolygonWallet; required super.walletInfo,
required super.password,
abstract class PolygonWalletBase super.mnemonic,
extends WalletBase<ERC20Balance, PolygonTransactionHistory, PolygonTransactionInfo> with Store { super.initialBalance,
PolygonWalletBase({ super.privateKey,
required WalletInfo walletInfo, required super.client,
String? mnemonic, }) : super(nativeCurrency: CryptoCurrency.maticpoly);
String? privateKey,
required String password,
ERC20Balance? initialBalance,
}) : syncStatus = const NotConnectedSyncStatus(),
_password = password,
_mnemonic = mnemonic,
_hexPrivateKey = privateKey,
_isTransactionUpdating = false,
_client = PolygonClient(),
walletAddresses = PolygonWalletAddresses(walletInfo),
balance = ObservableMap<CryptoCurrency, ERC20Balance>.of(
{CryptoCurrency.maticpoly: initialBalance ?? ERC20Balance(BigInt.zero)}),
super(walletInfo) {
this.walletInfo = walletInfo;
transactionHistory = PolygonTransactionHistory(walletInfo: walletInfo, password: password);
if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) {
CakeHive.registerAdapter(Erc20TokenAdapter());
}
_sharedPrefs.complete(SharedPreferences.getInstance());
}
final String? _mnemonic;
final String? _hexPrivateKey;
final String _password;
late final Box<Erc20Token> polygonErc20TokensBox;
late final EthPrivateKey _polygonPrivateKey;
late final PolygonClient _client;
EthPrivateKey get polygonPrivateKey => _polygonPrivateKey;
int? _gasPrice;
int? _estimatedGas;
bool _isTransactionUpdating;
// TODO: remove after integrating our own node and having eth_newPendingTransactionFilter
Timer? _transactionsUpdateTimer;
@override @override
WalletAddresses walletAddresses; Future<void> initErc20TokensBox() async {
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, ERC20Balance> balance;
final Completer<SharedPreferences> _sharedPrefs = Completer();
Future<void> init() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}"; final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}";
if (await CakeHive.boxExists(boxName)) { if (await CakeHive.boxExists(boxName)) {
polygonErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName); evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName);
} else { } else {
polygonErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName.replaceAll(" ", "")); evmChainErc20TokensBox = await CakeHive.openBox<Erc20Token>(boxName.replaceAll(" ", ""));
} }
await walletAddresses.init(); }
await transactionHistory.init();
_polygonPrivateKey = await getPrivateKey( @override
mnemonic: _mnemonic, void addInitialTokens() {
privateKey: _hexPrivateKey, final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens;
password: _password,
for (var token in initialErc20Tokens) {
evmChainErc20TokensBox.put(token.contractAddress, token);
}
}
@override
Future<bool> checkIfScanProviderIsEnabled() async {
bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true;
return isPolygonScanEnabled;
}
@override
String getTransactionHistoryFileName() => 'polygon_transactions.json';
@override
Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) {
return Erc20Token(
name: token.name,
symbol: token.symbol,
contractAddress: token.contractAddress,
decimal: token.decimal,
enabled: token.enabled,
tag: token.tag ?? "MATIC",
iconPath: iconPath,
); );
walletAddresses.address = _polygonPrivateKey.address.toString();
await save();
} }
@override @override
int calculateEstimatedFee(TransactionPriority priority, int? amount) { EVMChainTransactionInfo getTransactionInfo(
try { EVMChainTransactionModel transactionModel, String address) {
if (priority is PolygonTransactionPriority) { final model = PolygonTransactionInfo(
final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); id: transactionModel.hash,
return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); height: transactionModel.blockNumber,
} ethAmount: transactionModel.amount,
direction: transactionModel.from == address
return 0; ? TransactionDirection.outgoing
} catch (e) { : TransactionDirection.incoming,
return 0; isPending: false,
} date: transactionModel.date,
} confirmations: transactionModel.confirmations,
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
@override exponent: transactionModel.tokenDecimal ?? 18,
Future<void> changePassword(String password) { tokenSymbol: transactionModel.tokenSymbol ?? "MATIC",
throw UnimplementedError("changePassword"); to: transactionModel.to,
} from: transactionModel.from,
@override
void close() {
_client.stop();
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("Polygon Node connection failed");
}
_client.setListeners(_polygonPrivateKey.address, _onNewTransaction);
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final credentials0 = credentials as PolygonTransactionCredentials;
final outputs = credentials0.outputs;
final hasMultiDestination = outputs.length > 1;
final CryptoCurrency transactionCurrency =
balance.keys.firstWhere((element) => element.title == credentials0.currency.title);
final erc20Balance = balance[transactionCurrency]!;
BigInt totalAmount = BigInt.zero;
int exponent = transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18;
num amountToPolygonMultiplier = pow(10, exponent);
// so far this can not be made with Polygon as Polygon does not support multiple recipients
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw PolygonTransactionCreationException(transactionCurrency);
}
final totalOriginalAmount = PolygonFormatter.parsePolygonAmountToDouble(
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)));
totalAmount = BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
if (erc20Balance.balance < totalAmount) {
throw PolygonTransactionCreationException(transactionCurrency);
}
} else {
final output = outputs.first;
// since the fees are taken from Ethereum
// then no need to subtract the fees from the amount if send all
final BigInt allAmount;
if (transactionCurrency is Erc20Token) {
allAmount = erc20Balance.balance;
} else {
allAmount =
erc20Balance.balance - BigInt.from(calculateEstimatedFee(credentials0.priority!, null));
}
final totalOriginalAmount =
EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0);
totalAmount =
output.sendAll ? allAmount : BigInt.from(totalOriginalAmount * amountToPolygonMultiplier);
if (erc20Balance.balance < totalAmount) {
throw PolygonTransactionCreationException(transactionCurrency);
}
}
final pendingPolygonTransaction = await _client.signTransaction(
privateKey: _polygonPrivateKey,
toAddress: credentials0.outputs.first.isParsedAddress
? credentials0.outputs.first.extractedAddress!
: credentials0.outputs.first.address,
amount: totalAmount.toString(),
gas: _estimatedGas!,
priority: credentials0.priority!,
currency: transactionCurrency,
exponent: exponent,
contractAddress:
transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null,
); );
return model;
return pendingPolygonTransaction;
}
Future<void> _updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
}
bool isPolygonScanEnabled = (await _sharedPrefs.future).getBool("use_polygonscan") ?? true;
if (!isPolygonScanEnabled) {
return;
}
_isTransactionUpdating = true;
final transactions = await fetchTransactions();
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
} catch (_) {
_isTransactionUpdating = false;
}
} }
@override @override
Future<Map<String, PolygonTransactionInfo>> fetchTransactions() async { EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) {
final address = _polygonPrivateKey.address.hex; return PolygonTransactionHistory(walletInfo: walletInfo, password: password);
final transactions = await _client.fetchTransactions(address);
final List<Future<List<PolygonTransactionModel>>> polygonErc20TokensTransactions = [];
for (var token in balance.keys) {
if (token is Erc20Token) {
polygonErc20TokensTransactions.add(
_client.fetchTransactions(
address,
contractAddress: token.contractAddress,
),
);
}
}
final tokensTransaction = await Future.wait(polygonErc20TokensTransactions);
transactions.addAll(tokensTransaction.expand((element) => element));
final Map<String, PolygonTransactionInfo> result = {};
for (var transactionModel in transactions) {
if (transactionModel.isError) {
continue;
}
result[transactionModel.hash] = PolygonTransactionInfo(
id: transactionModel.hash,
height: transactionModel.blockNumber,
ethAmount: transactionModel.amount,
direction: transactionModel.from == address
? TransactionDirection.outgoing
: TransactionDirection.incoming,
isPending: false,
date: transactionModel.date,
confirmations: transactionModel.confirmations,
ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice,
exponent: transactionModel.tokenDecimal ?? 18,
tokenSymbol: transactionModel.tokenSymbol ?? "MATIC",
to: transactionModel.to,
);
}
return result;
} }
@override static Future<PolygonWallet> open(
Object get keys => throw UnimplementedError("keys"); {required String name, required String password, required WalletInfo walletInfo}) async {
@override
Future<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => HEX.encode(_polygonPrivateKey.privateKey);
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await _updateTransactions();
_gasPrice = await _client.getGasUnitPrice();
_estimatedGas = await _client.getEstimatedGas();
Timer.periodic(
const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice());
Timer.periodic(const Duration(seconds: 10),
(timer) async => _estimatedGas = await _client.getEstimatedGas());
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
static Future<PolygonWallet> open({
required String name,
required String password,
required WalletInfo walletInfo,
}) async {
final path = await pathForWallet(name: name, type: walletInfo.type); final path = await pathForWallet(name: name, type: walletInfo.type);
final jsonSource = await read(path: path, password: password); final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String?; final mnemonic = data['mnemonic'] as String?;
final privateKey = data['private_key'] as String?; final privateKey = data['private_key'] as String?;
final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ??
EVMChainERC20Balance(BigInt.zero);
return PolygonWallet( return PolygonWallet(
walletInfo: walletInfo, walletInfo: walletInfo,
@ -368,158 +111,7 @@ abstract class PolygonWalletBase
mnemonic: mnemonic, mnemonic: mnemonic,
privateKey: privateKey, privateKey: privateKey,
initialBalance: balance, initialBalance: balance,
client: PolygonClient(),
); );
} }
Future<void> _updateBalance() async {
balance[currency] = await _fetchMaticBalance();
await _fetchErc20Balances();
await save();
}
Future<ERC20Balance> _fetchMaticBalance() async {
final balance = await _client.getBalance(_polygonPrivateKey.address);
return ERC20Balance(balance.getInWei);
}
Future<void> _fetchErc20Balances() async {
for (var token in polygonErc20TokensBox.values) {
try {
if (token.enabled) {
balance[token] = await _client.fetchERC20Balances(
_polygonPrivateKey.address,
token.contractAddress,
);
} else {
balance.remove(token);
}
} catch (_) {}
}
}
Future<EthPrivateKey> getPrivateKey(
{String? mnemonic, String? privateKey, required String password}) async {
assert(mnemonic != null || privateKey != null);
if (privateKey != null) {
return EthPrivateKey.fromHex(privateKey);
}
final seed = bip39.mnemonicToSeed(mnemonic!);
final root = bip32.BIP32.fromSeed(seed);
const hdPathPolygon = "m/44'/60'/0'/0";
const index = 0;
final addressAtIndex = root.derivePath("$hdPathPolygon/$index");
return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>));
}
@override
Future<void>? updateBalance() async => await _updateBalance();
List<Erc20Token> get erc20Currencies => polygonErc20TokensBox.values.toList();
Future<void> addErc20Token(Erc20Token token) async {
String? iconPath;
try {
iconPath = CryptoCurrency.all
.firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase())
.iconPath;
} catch (_) {}
final token0 = Erc20Token(
name: token.name,
symbol: token.symbol,
contractAddress: token.contractAddress,
decimal: token.decimal,
enabled: token.enabled,
tag: token.tag ?? "POLY",
iconPath: iconPath,
);
await polygonErc20TokensBox.put(token0.contractAddress, token0);
if (token0.enabled) {
balance[token0] = await _client.fetchERC20Balances(
_polygonPrivateKey.address,
token0.contractAddress,
);
} else {
balance.remove(token0);
}
}
Future<void> deleteErc20Token(Erc20Token token) async {
await token.delete();
balance.remove(token);
_updateBalance();
}
Future<Erc20Token?> getErc20Token(String contractAddress) async =>
await _client.getErc20Token(contractAddress);
void _onNewTransaction() {
_updateBalance();
_updateTransactions();
}
void addInitialTokens() {
final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens;
for (var token in initialErc20Tokens) {
polygonErc20TokensBox.put(token.contractAddress, token);
}
}
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type);
final currentWalletFile = File(currentWalletPath);
final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName');
// Copies current wallet files into new wallet name's dir and files
if (currentWalletFile.existsSync()) {
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
await currentWalletFile.copy(newWalletPath);
}
if (currentTransactionsFile.existsSync()) {
final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName');
}
// Delete old name's dir and files
await Directory(currentDirPath).delete(recursive: true);
}
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
}
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_updateTransactions();
_updateBalance();
});
}
void updatePolygonScanUsageState(bool isEnabled) {
if (isEnabled) {
_updateTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
@override
String signMessage(String message, {String? address}) =>
bytesToHex(_polygonPrivateKey.signPersonalMessageToUint8List(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client();
} }

View file

@ -1,5 +0,0 @@
import 'package:cw_ethereum/ethereum_wallet_addresses.dart';
class PolygonWalletAddresses extends EthereumWalletAddresses {
PolygonWalletAddresses(super.walletInfo);
}

View file

@ -1,28 +0,0 @@
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
class PolygonNewWalletCredentials extends WalletCredentials {
PolygonNewWalletCredentials({required String name, WalletInfo? walletInfo})
: super(name: name, walletInfo: walletInfo);
}
class PolygonRestoreWalletFromSeedCredentials extends WalletCredentials {
PolygonRestoreWalletFromSeedCredentials(
{required String name,
required String password,
required this.mnemonic,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String mnemonic;
}
class PolygonRestoreWalletFromPrivateKey extends WalletCredentials {
PolygonRestoreWalletFromPrivateKey(
{required String name,
required String password,
required this.privateKey,
WalletInfo? walletInfo})
: super(name: name, password: password, walletInfo: walletInfo);
final String privateKey;
}

View file

@ -1,32 +1,34 @@
import 'dart:io';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_ethereum/ethereum_mnemonics.dart';
import 'package:cw_polygon/polygon_wallet.dart';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:hive/hive.dart'; import 'package:cw_core/wallet_base.dart';
import 'polygon_wallet_creation_credentials.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:collection/collection.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_evm/evm_chain_wallet_service.dart';
import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_mnemonics_exception.dart';
import 'package:cw_polygon/polygon_wallet.dart';
class PolygonWalletService extends WalletService<PolygonNewWalletCredentials, class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
PolygonRestoreWalletFromSeedCredentials, PolygonRestoreWalletFromPrivateKey> { PolygonWalletService(
PolygonWalletService(this.walletInfoSource); super.walletInfoSource, {
required this.client,
});
final Box<WalletInfo> walletInfoSource; late PolygonClient client;
@override @override
Future<PolygonWallet> create(PolygonNewWalletCredentials credentials) async { WalletType getType() => WalletType.polygon;
@override
Future<PolygonWallet> create(EVMChainNewWalletCredentials credentials) async {
final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final mnemonic = bip39.generateMnemonic(strength: strength); final mnemonic = bip39.generateMnemonic(strength: strength);
final wallet = PolygonWallet( final wallet = PolygonWallet(
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
mnemonic: mnemonic, mnemonic: mnemonic,
password: credentials.password!, password: credentials.password!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -36,18 +38,11 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
return wallet; return wallet;
} }
@override
WalletType getType() => WalletType.polygon;
@override
Future<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override @override
Future<PolygonWallet> openWallet(String name, String password) async { Future<PolygonWallet> openWallet(String name, String password) async {
final walletInfo = final walletInfo =
walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType()));
final wallet = await PolygonWalletBase.open( final wallet = await PolygonWallet.open(
name: name, name: name,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
@ -60,19 +55,13 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
} }
@override @override
Future<void> remove(String wallet) async { Future<PolygonWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}
@override
Future<PolygonWallet> restoreFromKeys(PolygonRestoreWalletFromPrivateKey credentials) async {
final wallet = PolygonWallet( final wallet = PolygonWallet(
password: credentials.password!, password: credentials.password!,
privateKey: credentials.privateKey, privateKey: credentials.privateKey,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -83,15 +72,17 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
} }
@override @override
Future<PolygonWallet> restoreFromSeed(PolygonRestoreWalletFromSeedCredentials credentials) async { Future<PolygonWallet> restoreFromSeed(
EVMChainRestoreWalletFromSeedCredentials credentials) async {
if (!bip39.validateMnemonic(credentials.mnemonic)) { if (!bip39.validateMnemonic(credentials.mnemonic)) {
throw EthereumMnemonicIsIncorrectException(); throw PolygonMnemonicIsIncorrectException();
} }
final wallet = PolygonWallet( final wallet = PolygonWallet(
password: credentials.password!, password: credentials.password!,
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
client: client,
); );
await wallet.init(); await wallet.init();
@ -105,7 +96,7 @@ class PolygonWalletService extends WalletService<PolygonNewWalletCredentials,
Future<void> rename(String currentName, String password, String newName) async { Future<void> rename(String currentName, String password, String newName) async {
final currentWalletInfo = walletInfoSource.values final currentWalletInfo = walletInfoSource.values
.firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType()));
final currentWallet = await PolygonWalletBase.open( final currentWallet = await PolygonWallet.open(
password: password, name: currentName, walletInfo: currentWalletInfo); password: password, name: currentName, walletInfo: currentWalletInfo);
await currentWallet.renameWalletFiles(newName); await currentWallet.renameWalletFiles(newName);

View file

@ -16,15 +16,12 @@ dependencies:
path: ../cw_core path: ../cw_core
cw_ethereum: cw_ethereum:
path: ../cw_ethereum path: ../cw_ethereum
mobx: ^2.0.7+4 cw_evm:
intl: ^0.18.0 path: ../cw_evm
bip39: ^1.0.6
hive: ^2.2.3
collection: ^1.17.1
web3dart: ^2.7.1 web3dart: ^2.7.1
bip32: ^2.0.0 hive: ^2.2.3
hex: ^0.2.0 bip39: ^1.0.6
shared_preferences: ^2.0.15 collection: ^1.17.1
dev_dependencies: dev_dependencies:
@ -32,8 +29,6 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
build_runner: ^2.1.11 build_runner: ^2.1.11
mobx_codegen: ^2.0.7
hive_generator: ^1.1.3
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -151,7 +151,7 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.unspentCoins; return bitcoinWallet.unspentCoins;
} }
void updateUnspents(Object wallet) async { Future<void> updateUnspents(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateUnspent(); await bitcoinWallet.updateUnspent();
} }

View file

@ -186,7 +186,10 @@ Future<void> defaultSettingsMigration(
await rewriteSecureStoragePin(secureStorage: secureStorage); await rewriteSecureStoragePin(secureStorage: secureStorage);
break; break;
case 26: case 26:
await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); /// commented out as it was a probable cause for some users to have white screen issues
/// maybe due to multiple access on Secure Storage at once
/// or long await time on start of the app
// await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences);
break; break;
default: default:
break; break;
@ -507,7 +510,7 @@ Future<void> changeLitecoinCurrentElectrumServerToDefault(
Future<void> changeBitcoinCashCurrentNodeToDefault( Future<void> changeBitcoinCashCurrentNodeToDefault(
{required SharedPreferences sharedPreferences, required Box<Node> nodes}) async { {required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); final server = getBitcoinCashDefaultElectrumServer(nodes: nodes);
final serverId = server?.key as int ?? 0; final serverId = server?.key as int? ?? 0;
await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId);
} }

View file

@ -2,17 +2,17 @@ part of 'ethereum.dart';
class CWEthereum extends Ethereum { class CWEthereum extends Ethereum {
@override @override
List<String> getEthereumWordList(String language) => EthereumMnemonics.englishWordlist; List<String> getEthereumWordList(String language) => EVMChainMnemonics.englishWordlist;
WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource) => WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource) =>
EthereumWalletService(walletInfoSource); EthereumWalletService(walletInfoSource, client: EthereumClient());
@override @override
WalletCredentials createEthereumNewWalletCredentials({ WalletCredentials createEthereumNewWalletCredentials({
required String name, required String name,
WalletInfo? walletInfo, WalletInfo? walletInfo,
}) => }) =>
EthereumNewWalletCredentials(name: name, walletInfo: walletInfo); EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo);
@override @override
WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ WalletCredentials createEthereumRestoreWalletFromSeedCredentials({
@ -20,7 +20,7 @@ class CWEthereum extends Ethereum {
required String mnemonic, required String mnemonic,
required String password, required String password,
}) => }) =>
EthereumRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
@override @override
WalletCredentials createEthereumRestoreWalletFromPrivateKey({ WalletCredentials createEthereumRestoreWalletFromPrivateKey({
@ -28,37 +28,37 @@ class CWEthereum extends Ethereum {
required String privateKey, required String privateKey,
required String password, required String password,
}) => }) =>
EthereumRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
@override @override
String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address;
@override @override
String getPrivateKey(WalletBase wallet) { String getPrivateKey(WalletBase wallet) {
final privateKeyHolder = (wallet as EthereumWallet).ethPrivateKey; final privateKeyHolder = (wallet as EthereumWallet).evmChainPrivateKey;
String stringKey = bytesToHex(privateKeyHolder.privateKey); String stringKey = bytesToHex(privateKeyHolder.privateKey);
return stringKey; return stringKey;
} }
@override @override
String getPublicKey(WalletBase wallet) { String getPublicKey(WalletBase wallet) {
final privateKeyInUnitInt = (wallet as EthereumWallet).ethPrivateKey; final privateKeyInUnitInt = (wallet as EthereumWallet).evmChainPrivateKey;
final publicKey = privateKeyInUnitInt.address.hex; final publicKey = privateKeyInUnitInt.address.hex;
return publicKey; return publicKey;
} }
@override @override
TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium; TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium;
@override @override
TransactionPriority getEthereumTransactionPrioritySlow() => EthereumTransactionPriority.slow; TransactionPriority getEthereumTransactionPrioritySlow() => EVMChainTransactionPriority.slow;
@override @override
List<TransactionPriority> getTransactionPriorities() => EthereumTransactionPriority.all; List<TransactionPriority> getTransactionPriorities() => EVMChainTransactionPriority.all;
@override @override
TransactionPriority deserializeEthereumTransactionPriority(int raw) => TransactionPriority deserializeEthereumTransactionPriority(int raw) =>
EthereumTransactionPriority.deserialize(raw: raw); EVMChainTransactionPriority.deserialize(raw: raw);
Object createEthereumTransactionCredentials( Object createEthereumTransactionCredentials(
List<Output> outputs, { List<Output> outputs, {
@ -66,7 +66,7 @@ class CWEthereum extends Ethereum {
required CryptoCurrency currency, required CryptoCurrency currency,
int? feeRate, int? feeRate,
}) => }) =>
EthereumTransactionCredentials( EVMChainTransactionCredentials(
outputs outputs
.map((out) => OutputInfo( .map((out) => OutputInfo(
fiatAmount: out.fiatAmount, fiatAmount: out.fiatAmount,
@ -79,7 +79,7 @@ class CWEthereum extends Ethereum {
formattedCryptoAmount: out.formattedCryptoAmount, formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo)) memo: out.memo))
.toList(), .toList(),
priority: priority as EthereumTransactionPriority, priority: priority as EVMChainTransactionPriority,
currency: currency, currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
@ -90,15 +90,15 @@ class CWEthereum extends Ethereum {
required CryptoCurrency currency, required CryptoCurrency currency,
required int feeRate, required int feeRate,
}) => }) =>
EthereumTransactionCredentials( EVMChainTransactionCredentials(
outputs, outputs,
priority: priority as EthereumTransactionPriority?, priority: priority as EVMChainTransactionPriority?,
currency: currency, currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
@override @override
int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); int formatterEthereumParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount);
@override @override
double formatterEthereumAmountToDouble( double formatterEthereumAmountToDouble(
@ -106,7 +106,7 @@ class CWEthereum extends Ethereum {
assert(transaction != null || amount != null); assert(transaction != null || amount != null);
if (transaction != null) { if (transaction != null) {
transaction as EthereumTransactionInfo; transaction as EVMChainTransactionInfo;
return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
} else { } else {
return (amount!) / BigInt.from(10).pow(exponent); return (amount!) / BigInt.from(10).pow(exponent);
@ -135,7 +135,7 @@ class CWEthereum extends Ethereum {
@override @override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as EthereumTransactionInfo; transaction as EVMChainTransactionInfo;
if (transaction.tokenSymbol == CryptoCurrency.eth.title) { if (transaction.tokenSymbol == CryptoCurrency.eth.title) {
return CryptoCurrency.eth; return CryptoCurrency.eth;
} }
@ -147,7 +147,7 @@ class CWEthereum extends Ethereum {
@override @override
void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) {
(wallet as EthereumWallet).updateEtherscanUsageState(isEnabled); (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled);
} }
@override @override

View file

@ -331,7 +331,7 @@ class CWMonero extends Monero {
} }
@override @override
void updateUnspents(Object wallet) async { Future<void> updateUnspents(Object wallet) async {
final moneroWallet = wallet as MoneroWallet; final moneroWallet = wallet as MoneroWallet;
await moneroWallet.updateUnspent(); await moneroWallet.updateUnspent();
} }

View file

@ -2,17 +2,17 @@ part of 'polygon.dart';
class CWPolygon extends Polygon { class CWPolygon extends Polygon {
@override @override
List<String> getPolygonWordList(String language) => EthereumMnemonics.englishWordlist; List<String> getPolygonWordList(String language) => EVMChainMnemonics.englishWordlist;
WalletService createPolygonWalletService(Box<WalletInfo> walletInfoSource) => WalletService createPolygonWalletService(Box<WalletInfo> walletInfoSource) =>
PolygonWalletService(walletInfoSource); PolygonWalletService(walletInfoSource, client: PolygonClient());
@override @override
WalletCredentials createPolygonNewWalletCredentials({ WalletCredentials createPolygonNewWalletCredentials({
required String name, required String name,
WalletInfo? walletInfo, WalletInfo? walletInfo,
}) => }) =>
PolygonNewWalletCredentials(name: name, walletInfo: walletInfo); EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo);
@override @override
WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ WalletCredentials createPolygonRestoreWalletFromSeedCredentials({
@ -20,7 +20,7 @@ class CWPolygon extends Polygon {
required String mnemonic, required String mnemonic,
required String password, required String password,
}) => }) =>
PolygonRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic);
@override @override
WalletCredentials createPolygonRestoreWalletFromPrivateKey({ WalletCredentials createPolygonRestoreWalletFromPrivateKey({
@ -28,37 +28,37 @@ class CWPolygon extends Polygon {
required String privateKey, required String privateKey,
required String password, required String password,
}) => }) =>
PolygonRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey);
@override @override
String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address;
@override @override
String getPrivateKey(WalletBase wallet) { String getPrivateKey(WalletBase wallet) {
final privateKeyHolder = (wallet as PolygonWallet).polygonPrivateKey; final privateKeyHolder = (wallet as PolygonWallet).evmChainPrivateKey;
String stringKey = bytesToHex(privateKeyHolder.privateKey); String stringKey = bytesToHex(privateKeyHolder.privateKey);
return stringKey; return stringKey;
} }
@override @override
String getPublicKey(WalletBase wallet) { String getPublicKey(WalletBase wallet) {
final privateKeyInUnitInt = (wallet as PolygonWallet).polygonPrivateKey; final privateKeyInUnitInt = (wallet as PolygonWallet).evmChainPrivateKey;
final publicKey = privateKeyInUnitInt.address.hex; final publicKey = privateKeyInUnitInt.address.hex;
return publicKey; return publicKey;
} }
@override @override
TransactionPriority getDefaultTransactionPriority() => PolygonTransactionPriority.medium; TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium;
@override @override
TransactionPriority getPolygonTransactionPrioritySlow() => PolygonTransactionPriority.slow; TransactionPriority getPolygonTransactionPrioritySlow() => EVMChainTransactionPriority.slow;
@override @override
List<TransactionPriority> getTransactionPriorities() => PolygonTransactionPriority.all; List<TransactionPriority> getTransactionPriorities() => EVMChainTransactionPriority.all;
@override @override
TransactionPriority deserializePolygonTransactionPriority(int raw) => TransactionPriority deserializePolygonTransactionPriority(int raw) =>
PolygonTransactionPriority.deserialize(raw: raw); EVMChainTransactionPriority.deserialize(raw: raw);
Object createPolygonTransactionCredentials( Object createPolygonTransactionCredentials(
List<Output> outputs, { List<Output> outputs, {
@ -66,7 +66,7 @@ class CWPolygon extends Polygon {
required CryptoCurrency currency, required CryptoCurrency currency,
int? feeRate, int? feeRate,
}) => }) =>
PolygonTransactionCredentials( EVMChainTransactionCredentials(
outputs outputs
.map((out) => OutputInfo( .map((out) => OutputInfo(
fiatAmount: out.fiatAmount, fiatAmount: out.fiatAmount,
@ -78,7 +78,7 @@ class CWPolygon extends Polygon {
isParsedAddress: out.isParsedAddress, isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount)) formattedCryptoAmount: out.formattedCryptoAmount))
.toList(), .toList(),
priority: priority as PolygonTransactionPriority, priority: priority as EVMChainTransactionPriority,
currency: currency, currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
@ -89,15 +89,15 @@ class CWPolygon extends Polygon {
required CryptoCurrency currency, required CryptoCurrency currency,
required int feeRate, required int feeRate,
}) => }) =>
PolygonTransactionCredentials( EVMChainTransactionCredentials(
outputs, outputs,
priority: priority as PolygonTransactionPriority?, priority: priority as EVMChainTransactionPriority?,
currency: currency, currency: currency,
feeRate: feeRate, feeRate: feeRate,
); );
@override @override
int formatterPolygonParseAmount(String amount) => PolygonFormatter.parsePolygonAmount(amount); int formatterPolygonParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount);
@override @override
double formatterPolygonAmountToDouble( double formatterPolygonAmountToDouble(
@ -105,7 +105,7 @@ class CWPolygon extends Polygon {
assert(transaction != null || amount != null); assert(transaction != null || amount != null);
if (transaction != null) { if (transaction != null) {
transaction as PolygonTransactionInfo; transaction as EVMChainTransactionInfo;
return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent);
} else { } else {
return (amount!) / BigInt.from(10).pow(exponent); return (amount!) / BigInt.from(10).pow(exponent);
@ -134,7 +134,7 @@ class CWPolygon extends Polygon {
@override @override
CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) {
transaction as PolygonTransactionInfo; transaction as EVMChainTransactionInfo;
if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) { if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) {
return CryptoCurrency.maticpoly; return CryptoCurrency.maticpoly;
} }
@ -146,7 +146,7 @@ class CWPolygon extends Polygon {
@override @override
void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) { void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) {
(wallet as PolygonWallet).updatePolygonScanUsageState(isEnabled); (wallet as PolygonWallet).updateScanProviderUsageState(isEnabled);
} }
@override @override

View file

@ -101,26 +101,26 @@ class NFTDetailsPage extends BasePage {
SizedBox(height: 16), SizedBox(height: 16),
_NFTSingleInfoTile( _NFTSingleInfoTile(
infoType: S.current.name, infoType: S.current.name,
infoValue: nftAsset.normalizedMetadata?.name ?? '', infoValue: nftAsset.normalizedMetadata?.name ?? '---',
), ),
if (nftAsset.normalizedMetadata?.description != null) ...[ if (nftAsset.normalizedMetadata?.description != null) ...[
SizedBox(height: 16), SizedBox(height: 16),
_NFTSingleInfoTile( _NFTSingleInfoTile(
infoType: 'Description', infoType: S.current.description,
infoValue: nftAsset.normalizedMetadata?.description ?? '', infoValue: nftAsset.normalizedMetadata?.description ?? '---',
), ),
], ],
SizedBox(height: 16), SizedBox(height: 16),
_NFTSingleInfoTile( _NFTSingleInfoTile(
infoType: 'Contract Name', infoType: S.current.contractName,
infoValue: nftAsset.name ?? '', infoValue: nftAsset.name ?? '---',
), ),
SizedBox(height: 8), SizedBox(height: 8),
_NFTSingleInfoTile( _NFTSingleInfoTile(
infoType: 'Contract Symbol', infoType: S.current.contractSymbol,
infoValue: nftAsset.symbol ?? '', infoValue: nftAsset.symbol ?? '---',
), ),
], ],
), ),

View file

@ -13,22 +13,15 @@ class NFTTileWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return InkWell(
onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, arguments: nftAsset),
arguments: nftAsset),
child: Container( child: Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.only(left: 16, right: 16), margin: const EdgeInsets.only(left: 16, right: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
border: Border.all( border: Border.all(
color: Theme.of(context) color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor, width: 1),
.extension<BalancePageTheme>()! color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
.cardBorderColor,
width: 1,
),
color: Theme.of(context)
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
), ),
child: Row( child: Row(
children: [ children: [
@ -40,14 +33,10 @@ class NFTTileWidget extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0), borderRadius: BorderRadius.circular(16.0),
border: Border.all( border: Border.all(
color: Theme.of(context) color: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
.extension<BalancePageTheme>()!
.cardBorderColor,
width: 1, width: 1,
), ),
color: Theme.of(context) color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
.extension<SyncIndicatorTheme>()!
.syncedBackgroundColor,
), ),
child: NFTImageWidget( child: NFTImageWidget(
imageUrl: nftAsset.normalizedMetadata?.imageUrl, imageUrl: nftAsset.normalizedMetadata?.imageUrl,
@ -59,27 +48,23 @@ class NFTTileWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'${nftAsset.name ?? ''} - ${nftAsset.symbol ?? ''}', '${nftAsset.name ?? '---'} - ${nftAsset.symbol ?? '---'}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: Theme.of(context) color: Theme.of(context).extension<BalancePageTheme>()!.labelTextColor,
.extension<BalancePageTheme>()!
.labelTextColor,
height: 1, height: 1,
), ),
), ),
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
nftAsset.normalizedMetadata?.name ?? nftAsset.name ?? "", nftAsset.normalizedMetadata?.name ?? nftAsset.name ?? "---",
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 20,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: Theme.of(context) color: Theme.of(context).extension<BalancePageTheme>()!.assetTitleColor,
.extension<BalancePageTheme>()!
.assetTitleColor,
height: 1, height: 1,
), ),
), ),
@ -92,4 +77,3 @@ class NFTTileWidget extends StatelessWidget {
); );
} }
} }

View file

@ -284,15 +284,17 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
if (state is TransactionCommitted) { if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>( if (context.mounted) {
context: context, showPopUp<void>(
builder: (BuildContext popupContext) { context: context,
return AlertWithOneAction( builder: (BuildContext popupContext) {
alertTitle: S.of(popupContext).sending, return AlertWithOneAction(
alertContent: S.of(popupContext).transaction_sent, alertTitle: S.of(popupContext).sending,
buttonText: S.of(popupContext).ok, alertContent: S.of(popupContext).transaction_sent,
buttonAction: () => Navigator.of(popupContext).pop()); buttonText: S.of(popupContext).ok,
}); buttonAction: () => Navigator.of(popupContext).pop());
});
}
}); });
} }
}); });

View file

@ -113,13 +113,15 @@ class AddressCell extends StatelessWidget {
color: textColor, color: textColor,
), ),
), ),
AutoSizeText( Flexible(
formattedAddress, child: AutoSizeText(
maxLines: 1, formattedAddress,
overflow: TextOverflow.ellipsis, maxLines: 1,
style: TextStyle( overflow: TextOverflow.ellipsis,
fontSize: isChange ? 10 : 14, style: TextStyle(
color: textColor, fontSize: isChange ? 10 : 14,
color: textColor,
),
), ),
), ),
], ],

View file

@ -53,16 +53,16 @@ class RootState extends State<Root> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addObserver(this);
bool value = await widget.authService.requireAuth();
setState(() { widget.authService.requireAuth().then((value) {
_requestAuth = value; WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() => _requestAuth = value);
}); });
}); });
_isInactiveController = StreamController<bool>.broadcast(); _isInactiveController = StreamController<bool>.broadcast();
_isInactive = false; _isInactive = false;
_postFrameCallback = false; _postFrameCallback = false;
WidgetsBinding.instance.addObserver(this);
super.initState(); super.initState();
if (DeviceInfo.instance.isMobile) { if (DeviceInfo.instance.isMobile) {
initUniLinks(); initUniLinks();

View file

@ -922,7 +922,7 @@ abstract class SettingsStoreBase with Store {
final allowBiometricalAuthentication = await SecureKey.getBool( final allowBiometricalAuthentication = await SecureKey.getBool(
secureStorage: secureStorage, secureStorage: secureStorage,
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
key: SecureKey.pinTimeOutDuration, key: SecureKey.allowBiometricalAuthenticationKey,
) ?? ) ??
false; false;
@ -1247,6 +1247,16 @@ abstract class SettingsStoreBase with Store {
) ?? ) ??
totpSecretKey; totpSecretKey;
final timeOutDuration = await SecureKey.getInt(
secureStorage: _secureStorage,
sharedPreferences: sharedPreferences,
key: SecureKey.pinTimeOutDuration,
);
pinTimeOutDuration = timeOutDuration != null
? PinCodeRequiredDuration.deserialize(raw: timeOutDuration)
: defaultPinCodeTimeOutDuration;
allowBiometricalAuthentication = await SecureKey.getBool( allowBiometricalAuthentication = await SecureKey.getBool(
secureStorage: _secureStorage, secureStorage: _secureStorage,
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,

View file

@ -239,6 +239,8 @@ abstract class TransactionDetailsViewModelBase with Store {
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
if (showRecipientAddress && tx.to != null) if (showRecipientAddress && tx.to != null)
StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!),
if (tx.direction == TransactionDirection.incoming && tx.from != null)
StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!),
]; ];
items.addAll(_items); items.addAll(_items);
@ -271,8 +273,10 @@ abstract class TransactionDetailsViewModelBase with Store {
StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()),
if (tx.feeFormatted()?.isNotEmpty ?? false) if (tx.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!),
if (showRecipientAddress && tx.to != null) if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing)
StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!),
if (tx.direction == TransactionDirection.incoming && tx.from != null)
StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!),
]; ];
items.addAll(_items); items.addAll(_items);

View file

@ -16,30 +16,17 @@ abstract class UnspentCoinsListViewModelBase with Store {
UnspentCoinsListViewModelBase( UnspentCoinsListViewModelBase(
{required this.wallet, required Box<UnspentCoinsInfo> unspentCoinsInfo}) {required this.wallet, required Box<UnspentCoinsInfo> unspentCoinsInfo})
: _unspentCoinsInfo = unspentCoinsInfo { : _unspentCoinsInfo = unspentCoinsInfo {
_updateUnspentCoinsInfo();
_updateUnspents(); _updateUnspents();
} }
WalletBase wallet; WalletBase wallet;
final Box<UnspentCoinsInfo> _unspentCoinsInfo; final Box<UnspentCoinsInfo> _unspentCoinsInfo;
@computed final ObservableList<UnspentCoinsItem> _items = ObservableList();
ObservableList<UnspentCoinsItem> get items => ObservableList.of(_getUnspents().map((elem) {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
return UnspentCoinsItem( @computed
address: elem.address, ObservableList<UnspentCoinsItem> get items => _items;
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
hash: elem.hash,
isFrozen: info.isFrozen,
note: info.note,
isSending: info.isSending,
amountRaw: elem.value,
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
);
}));
Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async { Future<void> saveUnspentCoinInfo(UnspentCoinsItem item) async {
try { try {
@ -77,9 +64,14 @@ abstract class UnspentCoinsListViewModelBase with Store {
} }
Future<void> _updateUnspents() async { Future<void> _updateUnspents() async {
if (wallet.type == WalletType.monero) return monero!.updateUnspents(wallet); if (wallet.type == WalletType.monero) {
if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) await monero!.updateUnspents(wallet);
return bitcoin!.updateUnspents(wallet); }
if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) {
await bitcoin!.updateUnspents(wallet);
}
_updateUnspentCoinsInfo();
} }
List<Unspent> _getUnspents() { List<Unspent> _getUnspents() {
@ -88,4 +80,26 @@ abstract class UnspentCoinsListViewModelBase with Store {
return bitcoin!.getUnspents(wallet); return bitcoin!.getUnspents(wallet);
return List.empty(); return List.empty();
} }
@action
void _updateUnspentCoinsInfo() {
_items.clear();
_items.addAll(_getUnspents().map((elem) {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
return UnspentCoinsItem(
address: elem.address,
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
hash: elem.hash,
isFrozen: info.isFrozen,
note: info.note,
isSending: info.isSending,
amountRaw: elem.value,
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
);
}));
}
} }

View file

@ -1,9 +1,10 @@
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_polygon && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -765,9 +765,12 @@
"transaction_details_source_address": "عنوان المصدر", "transaction_details_source_address": "عنوان المصدر",
"track": " ﺭﺎﺴﻣ", "track": " ﺭﺎﺴﻣ",
"pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ",
"contractName": "ﺪﻘﻌﻟﺍ ﻢﺳﺍ",
"contractSymbol": "ﺪﻘﻌﻟﺍ ﺰﻣﺭ",
"description": "ﻒﺻﻭ",
"camera_consent": ".ﻞﻴﺻﺎﻔﺘﻟﺍ ﻰﻠﻋ ﻝﻮﺼﺤﻠﻟ ﻢﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺔﻴﺻﻮﺼﺨﻟﺍ ﺔﺳﺎﻴﺳ ﻦﻣ ﻖﻘﺤﺘﻟﺍ ﻰﺟﺮﻳ .${provider} ﻝﻮﻠ", "camera_consent": ".ﻞﻴﺻﺎﻔﺘﻟﺍ ﻰﻠﻋ ﻝﻮﺼﺤﻠﻟ ﻢﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺔﻴﺻﻮﺼﺨﻟﺍ ﺔﺳﺎﻴﺳ ﻦﻣ ﻖﻘﺤﺘﻟﺍ ﻰﺟﺮﻳ .${provider} ﻝﻮﻠ",
"no_relays": " ﺕﻼﺣﺮﻤﻟﺍ ﻻ", "no_relays": "ﺕﻼﺣﺮﻤﻟﺍ ﻻ",
"choose_relay": " ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", "choose_relay": "ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ",
"no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ", "no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ",
"no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ" "no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ"
} }

View file

@ -761,6 +761,9 @@
"transaction_details_source_address": "Адрес на източника", "transaction_details_source_address": "Адрес на източника",
"track": "Писта", "track": "Писта",
"pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.",
"contractName": "Име на договора",
"contractSymbol": "Договор Символ",
"description": "Описание",
"camera_consent": "Вашият фотоапарат ще бъде използван за заснемане на изображение с цел идентификация от ${provider}. Моля, проверете тяхната политика за поверителност за подробности.", "camera_consent": "Вашият фотоапарат ще бъде използван за заснемане на изображение с цел идентификация от ${provider}. Моля, проверете тяхната политика за поверителност за подробности.",
"no_relays": "Без релета", "no_relays": "Без релета",
"choose_relay": "Моля, изберете реле, което да използвате", "choose_relay": "Моля, изберете реле, което да използвате",

View file

@ -761,6 +761,9 @@
"transaction_details_source_address": "Zdrojová adresa", "transaction_details_source_address": "Zdrojová adresa",
"track": "Dráha", "track": "Dráha",
"pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.",
"contractName": "Název smlouvy",
"contractSymbol": "Symbol smlouvy",
"description": "Popis",
"camera_consent": "Váš fotoaparát použije k pořízení snímku pro účely identifikace ${provider}. Podrobnosti najdete v jejich Zásadách ochrany osobních údajů.", "camera_consent": "Váš fotoaparát použije k pořízení snímku pro účely identifikace ${provider}. Podrobnosti najdete v jejich Zásadách ochrany osobních údajů.",
"no_relays": "Žádná relé", "no_relays": "Žádná relé",
"choose_relay": "Vyberte relé, které chcete použít", "choose_relay": "Vyberte relé, které chcete použít",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Quelladresse", "transaction_details_source_address": "Quelladresse",
"track": "Schiene", "track": "Schiene",
"pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.",
"contractName": "Vertragsname",
"contractSymbol": "Vertragssymbol",
"description": "Beschreibung",
"camera_consent": "Mit Ihrer Kamera wird bis zum ${provider} ein Bild zur Identifizierung aufgenommen. Weitere Informationen finden Sie in deren Datenschutzbestimmungen.", "camera_consent": "Mit Ihrer Kamera wird bis zum ${provider} ein Bild zur Identifizierung aufgenommen. Weitere Informationen finden Sie in deren Datenschutzbestimmungen.",
"no_relays": "Keine Relais", "no_relays": "Keine Relais",
"choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus",

View file

@ -770,6 +770,9 @@
"transaction_details_source_address": "Source address", "transaction_details_source_address": "Source address",
"track": "Track", "track": "Track",
"pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.",
"contractName": "Contract Name",
"contractSymbol": "Contract Symbol",
"description": "Description",
"camera_consent": "Your camera will be used to capture an image for identification purposes by ${provider}. Please check their Privacy Policy for details.", "camera_consent": "Your camera will be used to capture an image for identification purposes by ${provider}. Please check their Privacy Policy for details.",
"no_relays": "No relays", "no_relays": "No relays",
"choose_relay": "Please choose a relay to use", "choose_relay": "Please choose a relay to use",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Dirección de la fuente", "transaction_details_source_address": "Dirección de la fuente",
"track": "Pista", "track": "Pista",
"pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.",
"contractName": "Nombre del contrato",
"contractSymbol": "Símbolo de contrato",
"description": "Descripción",
"camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.", "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.",
"no_relays": "Sin relevos", "no_relays": "Sin relevos",
"choose_relay": "Por favor elija un relé para usar", "choose_relay": "Por favor elija un relé para usar",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Adresse source", "transaction_details_source_address": "Adresse source",
"track": "Piste", "track": "Piste",
"pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.",
"contractName": "Nom du contrat",
"contractSymbol": "Symbole du contrat",
"description": "Description",
"camera_consent": "Votre appareil photo sera utilisé pour capturer une image à des fins d'identification par ${provider}. Veuillez consulter leur politique de confidentialité pour plus de détails.", "camera_consent": "Votre appareil photo sera utilisé pour capturer une image à des fins d'identification par ${provider}. Veuillez consulter leur politique de confidentialité pour plus de détails.",
"no_relays": "Pas de relais", "no_relays": "Pas de relais",
"choose_relay": "Veuillez choisir un relais à utiliser", "choose_relay": "Veuillez choisir un relais à utiliser",

View file

@ -751,6 +751,9 @@
"transaction_details_source_address": "Adireshin Incord", "transaction_details_source_address": "Adireshin Incord",
"track": "Waƙa", "track": "Waƙa",
"pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.",
"contractName": "Sunan Kwangila",
"contractSymbol": "Alamar Kwangila",
"description": "Bayani",
"camera_consent": "Za a yi amfani da kyamarar ku don ɗaukar hoto don dalilai na tantancewa ta ${provider}. Da fatan za a duba Manufar Sirri don cikakkun bayanai.", "camera_consent": "Za a yi amfani da kyamarar ku don ɗaukar hoto don dalilai na tantancewa ta ${provider}. Da fatan za a duba Manufar Sirri don cikakkun bayanai.",
"no_relays": "Babu relays", "no_relays": "Babu relays",
"choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani", "choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "स्रोत पता", "transaction_details_source_address": "स्रोत पता",
"track": "रास्ता", "track": "रास्ता",
"pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।",
"contractName": "अनुबंध का नाम",
"contractSymbol": "अनुबंध चिह्न",
"description": "विवरण",
"camera_consent": "आपके कैमरे का उपयोग ${provider} द्वारा पहचान उद्देश्यों के लिए एक छवि कैप्चर करने के लिए किया जाएगा। विवरण के लिए कृपया उनकी गोपनीयता नीति जांचें।", "camera_consent": "आपके कैमरे का उपयोग ${provider} द्वारा पहचान उद्देश्यों के लिए एक छवि कैप्चर करने के लिए किया जाएगा। विवरण के लिए कृपया उनकी गोपनीयता नीति जांचें।",
"no_relays": "कोई रिले नहीं", "no_relays": "कोई रिले नहीं",
"choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें", "choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें",

View file

@ -767,6 +767,9 @@
"transaction_details_source_address": "Adresa izvora", "transaction_details_source_address": "Adresa izvora",
"track": "Staza", "track": "Staza",
"pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.",
"contractName": "Naziv ugovora",
"contractSymbol": "Simbol ugovora",
"description": "Opis",
"camera_consent": "Vaš će fotoaparat koristiti za snimanje slike u svrhu identifikacije od strane ${provider}. Pojedinosti potražite u njihovoj politici privatnosti.", "camera_consent": "Vaš će fotoaparat koristiti za snimanje slike u svrhu identifikacije od strane ${provider}. Pojedinosti potražite u njihovoj politici privatnosti.",
"no_relays": "Nema releja", "no_relays": "Nema releja",
"choose_relay": "Odaberite relej za korištenje", "choose_relay": "Odaberite relej za korištenje",

View file

@ -757,6 +757,9 @@
"transaction_details_source_address": "Alamat sumber", "transaction_details_source_address": "Alamat sumber",
"track": "Melacak", "track": "Melacak",
"pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.",
"contractName": "Nama Kontrak",
"contractSymbol": "Simbol Kontrak",
"description": "Keterangan",
"camera_consent": "Kamera Anda akan digunakan untuk mengambil gambar untuk tujuan identifikasi oleh ${provider}. Silakan periksa Kebijakan Privasi mereka untuk detailnya.", "camera_consent": "Kamera Anda akan digunakan untuk mengambil gambar untuk tujuan identifikasi oleh ${provider}. Silakan periksa Kebijakan Privasi mereka untuk detailnya.",
"no_relays": "Tidak ada relay", "no_relays": "Tidak ada relay",
"choose_relay": "Silakan pilih relai yang akan digunakan", "choose_relay": "Silakan pilih relai yang akan digunakan",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Indirizzo di partenza", "transaction_details_source_address": "Indirizzo di partenza",
"track": "Traccia", "track": "Traccia",
"pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.",
"contractName": "Nome del contratto",
"contractSymbol": "Simbolo del contratto",
"description": "Descrizione",
"camera_consent": "La tua fotocamera verrà utilizzata per acquisire un'immagine a scopo identificativo da ${provider}. Si prega di controllare la loro Informativa sulla privacy per i dettagli.", "camera_consent": "La tua fotocamera verrà utilizzata per acquisire un'immagine a scopo identificativo da ${provider}. Si prega di controllare la loro Informativa sulla privacy per i dettagli.",
"no_relays": "Nessun relè", "no_relays": "Nessun relè",
"choose_relay": "Scegli un relè da utilizzare", "choose_relay": "Scegli un relè da utilizzare",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "ソースアドレス", "transaction_details_source_address": "ソースアドレス",
"track": "追跡", "track": "追跡",
"pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。",
"contractName": "契約名",
"contractSymbol": "契約記号",
"description": "説明",
"camera_consent": "あなたのカメラは、${provider}_ までに識別目的で画像を撮影するために使用されます。詳細については、プライバシー ポリシーをご確認ください。", "camera_consent": "あなたのカメラは、${provider}_ までに識別目的で画像を撮影するために使用されます。詳細については、プライバシー ポリシーをご確認ください。",
"no_relays": "リレーなし", "no_relays": "リレーなし",
"choose_relay": "使用するリレーを選択してください", "choose_relay": "使用するリレーを選択してください",

View file

@ -767,6 +767,9 @@
"transaction_details_source_address": "소스 주소", "transaction_details_source_address": "소스 주소",
"track": "길", "track": "길",
"pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.",
"contractName": "계약명",
"contractSymbol": "계약 기호",
"description": "설명",
"camera_consent": "귀하의 카메라는 ${provider}의 식별 목적으로 이미지를 캡처하는 데 사용됩니다. 자세한 내용은 해당 개인정보 보호정책을 확인하세요.", "camera_consent": "귀하의 카메라는 ${provider}의 식별 목적으로 이미지를 캡처하는 데 사용됩니다. 자세한 내용은 해당 개인정보 보호정책을 확인하세요.",
"no_relays": "릴레이 없음", "no_relays": "릴레이 없음",
"choose_relay": "사용할 릴레이를 선택해주세요", "choose_relay": "사용할 릴레이를 선택해주세요",

View file

@ -767,6 +767,9 @@
"transaction_details_source_address": "အရင်းအမြစ်လိပ်စာ", "transaction_details_source_address": "အရင်းအမြစ်လိပ်စာ",
"track": "တစ်ပုဒ်", "track": "တစ်ပုဒ်",
"pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။",
"contractName": "စာချုပ်အမည်",
"contractSymbol": "စာချုပ်သင်္ကေတ",
"description": "ဖော်ပြချက်",
"camera_consent": "မှတ်ပုံတင်ခြင်းရည်ရွယ်ချက်များအတွက် ${provider} တွင် သင့်ကင်မရာကို အသုံးပြုပါမည်။ အသေးစိတ်အတွက် ၎င်းတို့၏ ကိုယ်ရေးကိုယ်တာမူဝါဒကို စစ်ဆေးပါ။", "camera_consent": "မှတ်ပုံတင်ခြင်းရည်ရွယ်ချက်များအတွက် ${provider} တွင် သင့်ကင်မရာကို အသုံးပြုပါမည်။ အသေးစိတ်အတွက် ၎င်းတို့၏ ကိုယ်ရေးကိုယ်တာမူဝါဒကို စစ်ဆေးပါ။",
"no_relays": "Relay မရှိပါ။", "no_relays": "Relay မရှိပါ။",
"choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", "choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Bron adres", "transaction_details_source_address": "Bron adres",
"track": "Spoor", "track": "Spoor",
"pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.",
"contractName": "Contractnaam",
"contractSymbol": "Contractsymbool",
"description": "Beschrijving",
"camera_consent": "Uw camera wordt gebruikt om vóór ${provider} een beeld vast te leggen voor identificatiedoeleinden. Raadpleeg hun privacybeleid voor meer informatie.", "camera_consent": "Uw camera wordt gebruikt om vóór ${provider} een beeld vast te leggen voor identificatiedoeleinden. Raadpleeg hun privacybeleid voor meer informatie.",
"no_relays": "Geen relais", "no_relays": "Geen relais",
"choose_relay": "Kies een relais dat u wilt gebruiken", "choose_relay": "Kies een relais dat u wilt gebruiken",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Adres źródłowy", "transaction_details_source_address": "Adres źródłowy",
"track": "Ścieżka", "track": "Ścieżka",
"pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.",
"contractName": "Nazwa umowy",
"contractSymbol": "Symbol kontraktu",
"description": "Opis",
"camera_consent": "Twój aparat zostanie użyty do przechwycenia obrazu w celach identyfikacyjnych przez ${provider}. Aby uzyskać szczegółowe informacje, sprawdź ich Politykę prywatności.", "camera_consent": "Twój aparat zostanie użyty do przechwycenia obrazu w celach identyfikacyjnych przez ${provider}. Aby uzyskać szczegółowe informacje, sprawdź ich Politykę prywatności.",
"no_relays": "Żadnych przekaźników", "no_relays": "Żadnych przekaźników",
"choose_relay": "Wybierz przekaźnik, którego chcesz użyć", "choose_relay": "Wybierz przekaźnik, którego chcesz użyć",

View file

@ -768,6 +768,9 @@
"transaction_details_source_address": "Endereço de Origem", "transaction_details_source_address": "Endereço de Origem",
"track": "Acompanhar", "track": "Acompanhar",
"pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.",
"contractName": "Nome do contrato",
"contractSymbol": "Símbolo do Contrato",
"description": "Descrição",
"camera_consent": "Sua câmera será usada para capturar uma imagem para fins de identificação por ${provider}. Por favor, verifique a Política de Privacidade para obter detalhes.", "camera_consent": "Sua câmera será usada para capturar uma imagem para fins de identificação por ${provider}. Por favor, verifique a Política de Privacidade para obter detalhes.",
"no_relays": "Sem relés", "no_relays": "Sem relés",
"choose_relay": "Escolha um relé para usar", "choose_relay": "Escolha um relé para usar",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Адрес источника", "transaction_details_source_address": "Адрес источника",
"track": "Отслеживать", "track": "Отслеживать",
"pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.",
"contractName": "Название контракта",
"contractSymbol": "Символ контракта",
"description": "Описание",
"camera_consent": "Ваша камера будет использоваться для захвата изображения в целях идентификации ${provider}. Пожалуйста, ознакомьтесь с их Политикой конфиденциальности для получения подробной информации.", "camera_consent": "Ваша камера будет использоваться для захвата изображения в целях идентификации ${provider}. Пожалуйста, ознакомьтесь с их Политикой конфиденциальности для получения подробной информации.",
"no_relays": "Нет реле", "no_relays": "Нет реле",
"choose_relay": "Пожалуйста, выберите реле для использования", "choose_relay": "Пожалуйста, выберите реле для использования",

View file

@ -767,6 +767,9 @@
"transaction_details_source_address": "ที่อยู่แหล่งกำเนิด", "transaction_details_source_address": "ที่อยู่แหล่งกำเนิด",
"track": "ติดตาม", "track": "ติดตาม",
"pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว",
"contractName": "ชื่อสัญญา",
"contractSymbol": "สัญลักษณ์สัญญา",
"description": "คำอธิบาย",
"camera_consent": "กล้องของคุณจะถูกนำมาใช้เพื่อจับภาพเพื่อวัตถุประสงค์ในการระบุตัวตนภายใน ${provider} โปรดตรวจสอบนโยบายความเป็นส่วนตัวเพื่อดูรายละเอียด", "camera_consent": "กล้องของคุณจะถูกนำมาใช้เพื่อจับภาพเพื่อวัตถุประสงค์ในการระบุตัวตนภายใน ${provider} โปรดตรวจสอบนโยบายความเป็นส่วนตัวเพื่อดูรายละเอียด",
"no_relays": "ไม่มีรีเลย์", "no_relays": "ไม่มีรีเลย์",
"choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้", "choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้",

View file

@ -763,6 +763,9 @@
"transaction_details_source_address": "SOURCE ADDRESS", "transaction_details_source_address": "SOURCE ADDRESS",
"track": "Subaybayan", "track": "Subaybayan",
"pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.",
"contractName": "Pangalan ng Kontrata",
"contractSymbol": "Simbolo ng Kontrata",
"description": "Paglalarawan",
"camera_consent": "Gagamitin ang iyong camera upang kumuha ng larawan para sa mga layunin ng pagkakakilanlan sa pamamagitan ng ${provider}. Pakisuri ang kanilang Patakaran sa Privacy para sa mga detalye.", "camera_consent": "Gagamitin ang iyong camera upang kumuha ng larawan para sa mga layunin ng pagkakakilanlan sa pamamagitan ng ${provider}. Pakisuri ang kanilang Patakaran sa Privacy para sa mga detalye.",
"no_relays": "Walang mga relay", "no_relays": "Walang mga relay",
"choose_relay": "Mangyaring pumili ng relay na gagamitin", "choose_relay": "Mangyaring pumili ng relay na gagamitin",

View file

@ -767,6 +767,9 @@
"transaction_details_source_address": "Kaynak adresi", "transaction_details_source_address": "Kaynak adresi",
"track": "İzlemek", "track": "İzlemek",
"pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.",
"contractName": "Sözleşme Adı",
"contractSymbol": "Sözleşme Sembolü",
"description": "Tanım",
"camera_consent": "Kameranız ${provider} tarihine kadar tanımlama amacıyla bir görüntü yakalamak için kullanılacaktır. Ayrıntılar için lütfen Gizlilik Politikalarını kontrol edin.", "camera_consent": "Kameranız ${provider} tarihine kadar tanımlama amacıyla bir görüntü yakalamak için kullanılacaktır. Ayrıntılar için lütfen Gizlilik Politikalarını kontrol edin.",
"no_relays": "Röle yok", "no_relays": "Röle yok",
"choose_relay": "Lütfen kullanmak için bir röle seçin", "choose_relay": "Lütfen kullanmak için bir röle seçin",

View file

@ -769,6 +769,9 @@
"transaction_details_source_address": "Адреса джерела", "transaction_details_source_address": "Адреса джерела",
"track": "Відслідковувати", "track": "Відслідковувати",
"pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.",
"contractName": "Назва контракту",
"contractSymbol": "Контракт символ",
"description": "опис",
"camera_consent": "Ваша камера використовуватиметься для зйомки зображення з метою ідентифікації ${provider}. Будь ласка, ознайомтеся з їхньою політикою конфіденційності, щоб дізнатися більше.", "camera_consent": "Ваша камера використовуватиметься для зйомки зображення з метою ідентифікації ${provider}. Будь ласка, ознайомтеся з їхньою політикою конфіденційності, щоб дізнатися більше.",
"no_relays": "Без реле", "no_relays": "Без реле",
"choose_relay": "Будь ласка, виберіть реле для використання", "choose_relay": "Будь ласка, виберіть реле для використання",

View file

@ -761,9 +761,12 @@
"transaction_details_source_address": "ماخذ ایڈریس", "transaction_details_source_address": "ماخذ ایڈریس",
"track": " ﮏﯾﺮﭨ", "track": " ﮏﯾﺮﭨ",
"pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ",
"contractName": "ﻡﺎﻧ ﺎﮐ ﮦﺪﮨﺎﻌﻣ",
"contractSymbol": "ﺖﻣﻼﻋ ﯽﮐ ﮦﺪﮨﺎﻌﻣ",
"description": "ﻞﯿﺼﻔﺗ",
"camera_consent": "۔ﮟﯿﮭﮑﯾﺩ ﯽﺴﯿﻟﺎﭘ ﯽﺴﯾﻮﯿﺋﺍﺮﭘ ﯽﮐ ﻥﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﺕﻼ${provider}ﯿﺼﻔﺗ ۔ﺎﮔ ﮯﺋﺎﺟ ﺎﯿﮐ ﻝﺎﻤﻌﺘﺳﺍ ﮯﯿﻟ", "camera_consent": "۔ﮟﯿﮭﮑﯾﺩ ﯽﺴﯿﻟﺎﭘ ﯽﺴﯾﻮﯿﺋﺍﺮﭘ ﯽﮐ ﻥﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﺕﻼ${provider}ﯿﺼﻔﺗ ۔ﺎﮔ ﮯﺋﺎﺟ ﺎﯿﮐ ﻝﺎﻤﻌﺘﺳﺍ ﮯﯿﻟ",
"no_relays": " ۔ﮟﯿﮩﻧ ﮯﻠﯾﺭ ﯽﺋﻮﮐ", "no_relays": "۔ﮟﯿﮩﻧ ﮯﻠﯾﺭ ﯽﺋﻮﮐ",
"choose_relay": " ۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ", "choose_relay": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ",
"no_relays_message": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮯﻠﯾﺭ ﮟﯿﻣ ﮈﺭﺎﮑﯾﺭ ﺮﭩﺳﻮﻧ ﮯﻨﭘﺍ ﮦﻭ ﮧﮐ ﮟﯾﺩ ﺖﯾﺍﺪﮨ ﻮﮐ ﮦﺪﻨﻨﮐ ﻝﻮﺻﻭ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", "no_relays_message": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮯﻠﯾﺭ ﮟﯿﻣ ﮈﺭﺎﮑﯾﺭ ﺮﭩﺳﻮﻧ ﮯﻨﭘﺍ ﮦﻭ ﮧﮐ ﮟﯾﺩ ﺖﯾﺍﺪﮨ ﻮﮐ ﮦﺪﻨﻨﮐ ﻝﻮﺻﻭ ﻡﺮﮐ ﮦﺍﺮﺑ ۔",
"no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ" "no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ"
} }

View file

@ -763,6 +763,9 @@
"transaction_details_source_address": "Adirẹsi orisun", "transaction_details_source_address": "Adirẹsi orisun",
"track": "Orin", "track": "Orin",
"pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.",
"contractName": "Orukọ adehun",
"contractSymbol": "Aami adehun",
"description": "Apejuwe",
"camera_consent": "Kamẹra rẹ yoo ṣee lo lati ya aworan kan fun awọn idi idanimọ nipasẹ ${provider}. Jọwọ ṣayẹwo Ilana Aṣiri wọn fun awọn alaye.", "camera_consent": "Kamẹra rẹ yoo ṣee lo lati ya aworan kan fun awọn idi idanimọ nipasẹ ${provider}. Jọwọ ṣayẹwo Ilana Aṣiri wọn fun awọn alaye.",
"no_relays": "Ko si relays", "no_relays": "Ko si relays",
"choose_relay": "Jọwọ yan yii lati lo", "choose_relay": "Jọwọ yan yii lati lo",

View file

@ -768,6 +768,9 @@
"transaction_details_source_address": "源地址", "transaction_details_source_address": "源地址",
"track": "追踪", "track": "追踪",
"pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。",
"contractName": "合约名称",
"contractSymbol": "合约符号",
"description": "描述",
"camera_consent": "${provider} 将使用您的相机拍摄图像以供识别之用。请查看他们的隐私政策了解详情。", "camera_consent": "${provider} 将使用您的相机拍摄图像以供识别之用。请查看他们的隐私政策了解详情。",
"no_relays": "无继电器", "no_relays": "无继电器",
"choose_relay": "请选择要使用的继电器", "choose_relay": "请选择要使用的继电器",

View file

@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN)
APP_ANDROID_TYPE=$1 APP_ANDROID_TYPE=$1
MONERO_COM_NAME="Monero.com" MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="1.10.0" MONERO_COM_VERSION="1.10.2"
MONERO_COM_BUILD_NUMBER=72 MONERO_COM_BUILD_NUMBER=74
MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_BUNDLE_ID="com.monero.app"
MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_PACKAGE="com.monero.app"
MONERO_COM_SCHEME="monero.com" MONERO_COM_SCHEME="monero.com"
CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="4.13.0" CAKEWALLET_VERSION="4.13.2"
CAKEWALLET_BUILD_NUMBER=189 CAKEWALLET_BUILD_NUMBER=191
CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet"
CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet"
CAKEWALLET_SCHEME="cakewallet" CAKEWALLET_SCHEME="cakewallet"

View file

@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN)
APP_IOS_TYPE=$1 APP_IOS_TYPE=$1
MONERO_COM_NAME="Monero.com" MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="1.10.0" MONERO_COM_VERSION="1.10.2"
MONERO_COM_BUILD_NUMBER=70 MONERO_COM_BUILD_NUMBER=72
MONERO_COM_BUNDLE_ID="com.cakewallet.monero" MONERO_COM_BUNDLE_ID="com.cakewallet.monero"
CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="4.13.0" CAKEWALLET_VERSION="4.13.2"
CAKEWALLET_BUILD_NUMBER=208 CAKEWALLET_BUILD_NUMBER=210
CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet"
HAVEN_NAME="Haven" HAVEN_NAME="Haven"

View file

@ -16,13 +16,13 @@ if [ -n "$1" ]; then
fi fi
MONERO_COM_NAME="Monero.com" MONERO_COM_NAME="Monero.com"
MONERO_COM_VERSION="1.0.0" MONERO_COM_VERSION="1.0.2"
MONERO_COM_BUILD_NUMBER=1 MONERO_COM_BUILD_NUMBER=4
MONERO_COM_BUNDLE_ID="com.cakewallet.monero" MONERO_COM_BUNDLE_ID="com.cakewallet.monero"
CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_NAME="Cake Wallet"
CAKEWALLET_VERSION="1.6.0" CAKEWALLET_VERSION="1.6.2"
CAKEWALLET_BUILD_NUMBER=50 CAKEWALLET_BUILD_NUMBER=52
CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet"
if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then

View file

@ -127,7 +127,7 @@ abstract class Bitcoin {
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate);
List<Unspent> getUnspents(Object wallet); List<Unspent> getUnspents(Object wallet);
void updateUnspents(Object wallet); Future<void> updateUnspents(Object wallet);
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource); WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource);
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource); WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource);
TransactionPriority getBitcoinTransactionPriorityMedium(); TransactionPriority getBitcoinTransactionPriorityMedium();
@ -270,7 +270,7 @@ abstract class Monero {
List<String> getMoneroWordList(String language); List<String> getMoneroWordList(String language);
List<Unspent> getUnspents(Object wallet); List<Unspent> getUnspents(Object wallet);
void updateUnspents(Object wallet); Future<void> updateUnspents(Object wallet);
WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ WalletCredentials createMoneroRestoreWalletFromKeysCredentials({
required String name, required String name,
@ -525,19 +525,24 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
""";
const ethereumCWHeaders = """
import 'package:cw_evm/evm_chain_formatter.dart';
import 'package:cw_evm/evm_chain_mnemonics.dart';
import 'package:cw_evm/evm_chain_transaction_credentials.dart';
import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_ethereum/ethereum_wallet.dart';
import 'package:cw_ethereum/ethereum_wallet_service.dart';
import 'package:eth_sig_util/util/utils.dart'; import 'package:eth_sig_util/util/utils.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:web3dart/web3dart.dart'; import 'package:web3dart/web3dart.dart';
""";
const ethereumCWHeaders = """
import 'package:cw_ethereum/ethereum_formatter.dart';
import 'package:cw_ethereum/ethereum_mnemonics.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_creation_credentials.dart';
import 'package:cw_ethereum/ethereum_wallet_service.dart';
import 'package:cw_ethereum/ethereum_transaction_priority.dart';
"""; """;
const ethereumCwPart = "part 'cw_ethereum.dart';"; const ethereumCwPart = "part 'cw_ethereum.dart';";
const ethereumContent = """ const ethereumContent = """
@ -612,19 +617,24 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:hive/hive.dart';
import 'package:web3dart/web3dart.dart';
"""; """;
const polygonCWHeaders = """ const polygonCWHeaders = """
import 'package:cw_polygon/polygon_formatter.dart'; import 'package:cw_evm/evm_chain_formatter.dart';
import 'package:cw_polygon/polygon_transaction_credentials.dart'; import 'package:cw_evm/evm_chain_mnemonics.dart';
import 'package:cw_polygon/polygon_transaction_info.dart'; import 'package:cw_evm/evm_chain_transaction_info.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_transaction_credentials.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_polygon/polygon_client.dart';
import 'package:cw_polygon/polygon_wallet.dart'; import 'package:cw_polygon/polygon_wallet.dart';
import 'package:cw_polygon/polygon_wallet_creation_credentials.dart';
import 'package:cw_polygon/polygon_wallet_service.dart'; import 'package:cw_polygon/polygon_wallet_service.dart';
import 'package:cw_polygon/polygon_transaction_priority.dart';
import 'package:cw_ethereum/ethereum_mnemonics.dart'; import 'package:hive/hive.dart';
import 'package:web3dart/web3dart.dart';
import 'package:eth_sig_util/util/utils.dart';
"""; """;
const polygonCwPart = "part 'cw_polygon.dart';"; const polygonCwPart = "part 'cw_polygon.dart';";
const polygonContent = """ const polygonContent = """
@ -924,6 +934,10 @@ Future<void> generatePubspec(
cw_polygon: cw_polygon:
path: ./cw_polygon path: ./cw_polygon
"""; """;
const cwEVM = """
cw_evm:
path: ./cw_evm
""";
final inputFile = File(pubspecOutputPath); final inputFile = File(pubspecOutputPath);
final inputText = await inputFile.readAsString(); final inputText = await inputFile.readAsString();
final inputLines = inputText.split('\n'); final inputLines = inputText.split('\n');
@ -964,6 +978,10 @@ Future<void> generatePubspec(
output += '\n$cwHaven'; output += '\n$cwHaven';
} }
if (hasEthereum || hasPolygon) {
output += '\n$cwEVM';
}
final outputLines = output.split('\n'); final outputLines = output.split('\n');
inputLines.insertAll(dependenciesIndex + 1, outputLines); inputLines.insertAll(dependenciesIndex + 1, outputLines);
final outputContent = inputLines.join('\n'); final outputContent = inputLines.join('\n');

Some files were not shown because too many files have changed in this diff Show more