diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e496201aa..da2859286 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,110 +6,110 @@ defaults: run: shell: bash jobs: - lint: - runs-on: [Linux, amd64, android] - container: - image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly - env: - STORE_PASS: test@cake_wallet - KEY_PASS: test@cake_wallet - MONEROC_CACHE_DIR_ROOT: /opt/generic_cache - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - ANDROID_AVD_HOME: /root/.android/avd - volumes: - - /opt/cw_cache_android/root/.cache:/root/.cache - - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd - - /opt/cw_cache_android/root/.ccache:/root/.ccache - - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache - - /opt/cw_cache_android/root/.gradle/:/root/.gradle - - /opt/cw_cache_android/root/.android/:/root/.android - - /opt/cw_cache_android/root/go/pkg:/root/go/pkg - - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache - - /dev/kvm:/dev/kvm - - /var/run/docker.sock:/var/run/docker.sock - strategy: - matrix: - api-level: [ 29 ] - - steps: - - name: Fix github actions messing up $HOME... - run: "echo HOME=/root | sudo tee -a $GITHUB_ENV" - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref }} - - - name: configure git - run: | - git config --global --add safe.directory '*' - git config --global user.email "ci@cakewallet.com" - git config --global user.name "CakeWallet CI" - - - name: Fetch prebuilt Torch - run: | - set -x -e - pushd scripts - wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz - rm -rf torch_dart - mkdir torch_dart - pushd torch_dart - tar -xzf ../torch_dart.tar.gz - popd - rm ./torch_dart.tar.gz - popd - - - name: Fetch prebuilt Reown - run: | - set -x -e - pushd scripts - # cleaning - rm -rf reown_flutter - rm -f reown_flutter.tar.gz - - wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz - mkdir reown_flutter - pushd reown_flutter - tar -xzf ../reown_flutter.tar.gz - popd - rm ./reown_flutter.tar.gz - popd - - - name: Build Bitbox Flutter - run: | - set -x -e - pushd scripts - ./build_bitbox_flutter.sh - popd - - - name: Prepare Zcash - run: | - set -x -e - scripts/prepare_zcash.sh - - - name: Execute Build and Setup Commands - run: | - pushd scripts/android - source ./app_env.sh cakewallet - export GITHUB_HUH=yeah - ./app_config.sh - popd - - - name: Install Flutter dependencies - run: | - flutter pub get - - - name: Build generated code - run: | - flutter --version - flutter clean - rm -rf .dart_tool - rm pubspec.lock - flutter pub get - ./model_generator.sh async - - - name: Generate localization - run: | - dart run tool/generate_localization.dart - - - name: Lint - run: | - ABORT_ON_CHANGE=yeah scripts/lint.sh \ No newline at end of file +# lint: +# runs-on: [Linux, amd64, android] +# container: +# image: ghcr.io/cake-tech/cake_wallet:debian13-flutter3.41.9-ndkr28-go1.24.1-ruststablenightly +# env: +# STORE_PASS: test@cake_wallet +# KEY_PASS: test@cake_wallet +# MONEROC_CACHE_DIR_ROOT: /opt/generic_cache +# BRANCH_NAME: ${{ github.head_ref || github.ref_name }} +# ANDROID_AVD_HOME: /root/.android/avd +# volumes: +# - /opt/cw_cache_android/root/.cache:/root/.cache +# - /opt/cw_cache_android/root/.android/avd/:/root/.android/avd +# - /opt/cw_cache_android/root/.ccache:/root/.ccache +# - /opt/cw_cache_android/root/.pub-cache/:/root/.pub-cache +# - /opt/cw_cache_android/root/.gradle/:/root/.gradle +# - /opt/cw_cache_android/root/.android/:/root/.android +# - /opt/cw_cache_android/root/go/pkg:/root/go/pkg +# - /opt/cw_cache_android/opt/generic_cache:/opt/generic_cache +# - /dev/kvm:/dev/kvm +# - /var/run/docker.sock:/var/run/docker.sock +# strategy: +# matrix: +# api-level: [ 29 ] +# +# steps: +# - name: Fix github actions messing up $HOME... +# run: "echo HOME=/root | sudo tee -a $GITHUB_ENV" +# - uses: actions/checkout@v4 +# with: +# ref: ${{ inputs.ref }} +# +# - name: configure git +# run: | +# git config --global --add safe.directory '*' +# git config --global user.email "ci@cakewallet.com" +# git config --global user.name "CakeWallet CI" +# +# - name: Fetch prebuilt Torch +# run: | +# set -x -e +# pushd scripts +# wget https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -O torch_dart.tar.gz +# rm -rf torch_dart +# mkdir torch_dart +# pushd torch_dart +# tar -xzf ../torch_dart.tar.gz +# popd +# rm ./torch_dart.tar.gz +# popd +# +# - name: Fetch prebuilt Reown +# run: | +# set -x -e +# pushd scripts +# # cleaning +# rm -rf reown_flutter +# rm -f reown_flutter.tar.gz +# +# wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz +# mkdir reown_flutter +# pushd reown_flutter +# tar -xzf ../reown_flutter.tar.gz +# popd +# rm ./reown_flutter.tar.gz +# popd +# +# - name: Build Bitbox Flutter +# run: | +# set -x -e +# pushd scripts +# ./build_bitbox_flutter.sh +# popd +# +# - name: Prepare Zcash +# run: | +# set -x -e +# scripts/prepare_zcash.sh +# +# - name: Execute Build and Setup Commands +# run: | +# pushd scripts/android +# source ./app_env.sh cakewallet +# export GITHUB_HUH=yeah +# ./app_config.sh +# popd +# +# - name: Install Flutter dependencies +# run: | +# flutter pub get +# +# - name: Build generated code +# run: | +# flutter --version +# flutter clean +# rm -rf .dart_tool +# rm pubspec.lock +# flutter pub get +# ./model_generator.sh async +# +# - name: Generate localization +# run: | +# dart run tool/generate_localization.dart +# +# - name: Lint +# run: | +# ABORT_ON_CHANGE=yeah scripts/lint.sh \ No newline at end of file diff --git a/cw_core/lib/amount/exchange_rate.dart b/cw_core/lib/amount/exchange_rate.dart new file mode 100644 index 000000000..efcb06abd --- /dev/null +++ b/cw_core/lib/amount/exchange_rate.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/currency.dart'; + +class ExchangeRate { + /// The currency being priced (e.g. BTC in a BTC/USD pair). + final Currency base; + + /// The price of one whole unit of [base], in the quote currency + /// (e.g. 45000 USD in a BTC/USD pair). + final Money quote; + + const ExchangeRate({required this.base, required this.quote}); + + /// Converts [amount] between the base and quote currencies. + /// + /// Results are truncated toward zero. + /// + /// Throws an [ArgumentError] if [amount]'s currency is not part of + /// this pair. + Money convert(Money amount) { + if (base == quote.currency && base == amount.currency) return amount; + + final scale = BigInt.from(10).pow(base.decimals); + + if (amount.currency == base) { + if (quote.isZero) return Money.zero(quote.currency); + + return Money(amount.amount * quote.amount ~/ scale, quote.currency); + } + + if (amount.currency == quote.currency) { + if (quote.isZero) return Money.zero(base); + + return Money(amount.amount * scale ~/ quote.amount, base); + } + + throw ArgumentError( + "Unable to convert ${amount.currency.symbol} in ${base.symbol}/${quote.currency.symbol} pair"); + } +} diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart index 3ff5af928..9c2332310 100644 --- a/cw_core/lib/amount/money.dart +++ b/cw_core/lib/amount/money.dart @@ -191,6 +191,10 @@ class Money implements Comparable { @override int get hashCode => amount.hashCode ^ currency.hashCode; + // Added this to reduce the hops we do to convert Money to double + // for fiat conversion and display + double toDouble() => amount / multiplierOf(currency.decimals); + @override String toString() => formatFixed(amount, currency.decimals); diff --git a/cw_core/lib/format_fixed.dart b/cw_core/lib/format_fixed.dart index 52c0e1470..aa0162812 100644 --- a/cw_core/lib/format_fixed.dart +++ b/cw_core/lib/format_fixed.dart @@ -4,13 +4,11 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri decimals ??= 0; fractionalDigits ??= decimals; - var multiplier = getMultiplier(decimals); - // Make sure wei is a big number (convert as necessary) + final multiplier = multiplierOf(decimals); var negative = value.isNegative; - if (negative) value = value * BigInt.from(-1); + if (negative) value = -value; - var fraction = - value.modPow(BigInt.one, BigInt.parse(multiplier)).toString().padLeft(decimals, "0"); + var fraction = (value % multiplier).toString().padLeft(decimals, "0"); if (fractionalDigits < 0) fractionalDigits = 0; if (fractionalDigits > decimals) fractionalDigits = decimals; @@ -20,7 +18,7 @@ String formatFixed(BigInt value, int? decimals, {int? fractionalDigits, bool tri fraction = removeTrailing("0", fraction); } - final whole = (value ~/ BigInt.parse(multiplier)); + final whole = value ~/ multiplier; final valString = fraction.isEmpty ? "$whole" : "$whole.$fraction"; diff --git a/cw_core/lib/parse_fixed.dart b/cw_core/lib/parse_fixed.dart index 76c74dc9f..dbbc24f7c 100644 --- a/cw_core/lib/parse_fixed.dart +++ b/cw_core/lib/parse_fixed.dart @@ -58,7 +58,7 @@ BigInt parseFixed(String value, int decimals) { final wholeValue = BigInt.parse(whole); final fractionValue = BigInt.parse(fraction); - final multiplierValue = BigInt.parse(multiplier); + final multiplierValue = multiplierOf(decimals); var wei = (wholeValue * multiplierValue) + fractionValue; @@ -69,3 +69,8 @@ BigInt parseFixed(String value, int decimals) { // Returns a string "1" followed by decimal "0"s String getMultiplier(int decimals) => "1".padRight(decimals + 1, "0"); + +final _multipliers = {}; + +// this is more direct and faster than having it as string then parsing everytime to get the number +BigInt multiplierOf(int decimals) => _multipliers[decimals] ??= BigInt.from(10).pow(decimals); diff --git a/cw_core/test/amount/exchange_rate_test.dart b/cw_core/test/amount/exchange_rate_test.dart new file mode 100644 index 000000000..3873e349f --- /dev/null +++ b/cw_core/test/amount/exchange_rate_test.dart @@ -0,0 +1,147 @@ +import 'package:cw_core/amount/exchange_rate.dart'; +import 'package:cw_core/amount/money.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ExchangeRate', () { + final rate = ExchangeRate( + base: CryptoCurrency.btc, + quote: Money.parse("60000", EUR), + ); + + test('convert base currency to quote currency: 1 BTC to 60.000 EUR', () { + final result = rate.convert(Money.parse('1', CryptoCurrency.btc)); + expect(result, Money.parse("60000.00", EUR)); + }); + + test('convert base currency to quote currency: 0.5 BTC to 30.000 EUR', () { + final result = rate.convert(Money.parse('0.5', CryptoCurrency.btc)); + expect(result, Money.parse("30000.00", EUR)); + }); + + test('convert quote currency to base currency: 60.000 EUR to 1 BTC', () { + final result = rate.convert(Money.parse("60000.00", EUR)); + expect(result, Money.parse('1', CryptoCurrency.btc)); + }); + + test('convert quote currency to base currency: 30 EUR to 0.0005 BTC', () { + final result = rate.convert(Money.parse("30.00", EUR)); + expect(result, Money.parse('0.0005', CryptoCurrency.btc)); + }); + }); + + group('Edge Cases', () { + final rate = ExchangeRate( + base: CryptoCurrency.btc, + quote: Money.parse("60000", EUR), + ); + + test('Converting zero turns into zero', () { + expect(rate.convert(Money.zero(CryptoCurrency.btc)), Money.zero(EUR)); + expect(rate.convert(Money.zero(EUR)), Money.zero(CryptoCurrency.btc)); + }); + + test('truncate to smallest unit', () { + final result = rate.convert(Money(BigInt.one, CryptoCurrency.btc)); // 1 sat + expect(result, Money.zero(EUR)); + }); + + test('Truncate base unit of base currency', () { + final result = rate.convert(Money(BigInt.one, EUR)); + expect(result, Money.fromInt(16, CryptoCurrency.btc)); + }); + + test('No int overflow with large numbers', () { + final allBtc = Money.parse('21000000', CryptoCurrency.btc); + final result = rate.convert(allBtc); + expect(result, Money(BigInt.from(126000000000000), EUR)); + }); + + test('truncation for negative amounts', () { + final result = rate.convert(Money.fromInt(-1, EUR)); + expect(result, Money.fromInt(-16, CryptoCurrency.btc)); + }); + + test('Base currency without decimals places: JPY', () { + final rate = ExchangeRate( + base: JPY, + quote: Money.fromInt(1, EUR), + ); + + expect(rate.convert(Money.fromInt(500, JPY)), Money.fromInt(500, EUR)); + expect(rate.convert(Money.fromInt(500, EUR)), Money.fromInt(500, JPY)); + }); + + test('Base currency with 18 decimals places: ETH', () { + final rate = ExchangeRate( + base: CryptoCurrency.eth, + quote: Money.fromInt(300000, EUR), + ); + + expect(rate.convert(Money.parse('1', CryptoCurrency.eth)), Money.fromInt(300000, EUR)); + expect(rate.convert(Money.fromInt(3000, EUR)), Money.parse('0.01', CryptoCurrency.eth)); + }); + + test('Smalles base unit is less than smalles base unit of quote', () { + final rate = ExchangeRate( + base: CryptoCurrency.eth, + quote: Money.fromInt(300000, EUR), + ); + expect(rate.convert(Money(BigInt.one, CryptoCurrency.eth)), Money(BigInt.zero, EUR)); + }); + + test('Cannot convert currency outside of pair', () { + expect(() => rate.convert(Money.parse("60000", USD)), throwsArgumentError); + }); + + test('handle quote being 0 correctly', () { + final zeroRate = ExchangeRate( + base: CryptoCurrency.btc, + quote: Money(BigInt.zero, EUR), + ); + expect(zeroRate.convert(Money.fromInt(100, CryptoCurrency.btc)), Money(BigInt.zero, EUR)); + expect(zeroRate.convert(Money.fromInt(100, EUR)), Money(BigInt.zero, CryptoCurrency.btc)); + }); + }); +} + +class FiatCurrency implements Currency { + const FiatCurrency({ + required this.symbol, + required this.countryCode, + required this.fullName, + this.decimals = 2, + }); + + final String countryCode; + + @override + final String fullName; + + @override + final int decimals; + + @override + final String symbol; + + @override + String? get iconPath => throw UnimplementedError(); + + @override + String get name => throw UnimplementedError(); + + @override + String? get tag => throw UnimplementedError(); + + @override + Money parseAmount(String value) => Money.parse(value, this); + + @override + Money? tryParseAmount(String value) => Money.tryParse(value, this); +} + +const EUR = FiatCurrency(symbol: 'EUR', countryCode: "eur", fullName: "Euro"); +const USD = FiatCurrency(symbol: 'USD', countryCode: "usd", fullName: "US Dollar"); +const JPY = FiatCurrency(symbol: 'JPY', countryCode: "jpn", fullName: "Japanese Yen", decimals: 0); diff --git a/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart b/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart index 69d0ea7f7..aaea81542 100644 --- a/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart +++ b/cw_custom_lints/lib/http_force_proxy/http_force_proxy_rule.dart @@ -51,8 +51,7 @@ class _Visitor extends SimpleAstVisitor { if (uriString.startsWith("package:http")) { final prefixNode = node.prefix; - if (prefixNode != null && - prefixNode.name == "very_insecure_http_do_not_use") { + if (prefixNode != null && prefixNode.name == "very_insecure_http_do_not_use") { return; } diff --git a/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart b/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart index b7a2daf52..f33af29da 100644 --- a/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart +++ b/cw_custom_lints/lib/print_verbose/print_verbose_fix.dart @@ -17,8 +17,7 @@ class ReplaceWithPrintV extends ResolvedCorrectionProducer { ); @override - CorrectionApplicability get applicability => - CorrectionApplicability.singleLocation; + CorrectionApplicability get applicability => CorrectionApplicability.singleLocation; @override FixKind get fixKind => _fixKind; diff --git a/cw_shared_external/lib/cw_shared_external.dart b/cw_shared_external/lib/cw_shared_external.dart index 1d9df69a2..92b99b3c4 100644 --- a/cw_shared_external/lib/cw_shared_external.dart +++ b/cw_shared_external/lib/cw_shared_external.dart @@ -1,11 +1,9 @@ - import 'dart:async'; import 'package:flutter/services.dart'; class CwSharedExternal { - static const MethodChannel _channel = - const MethodChannel('cw_shared_external'); + static const MethodChannel _channel = const MethodChannel('cw_shared_external'); static Future get platformVersion async { final String version = (await _channel.invokeMethod('getPlatformVersion')).toString(); diff --git a/cw_solana/lib/solana_client.dart b/cw_solana/lib/solana_client.dart index b166a3505..aa79789dc 100644 --- a/cw_solana/lib/solana_client.dart +++ b/cw_solana/lib/solana_client.dart @@ -32,14 +32,29 @@ class TransactionFetchResult { }); } +class TransactionSyncResult { + final List transactions; + final String? newestSignature; + + TransactionSyncResult({ + required this.transactions, + this.newestSignature, + }); +} + class SolanaWalletClient { // Minimum amount in SOL to consider a transaction valid (to filter spam) static Money minValidAmount = Money.parse("0.00000003", CryptoCurrency.sol); + + static const int _signaturePageSize = 1000; + late final client = ProxyWrapper().getHttpIOClient(); SolanaRPC? _provider; + bool _isStopped = false; bool connect(Node node) { try { + _isStopped = false; String formattedUrl; String protocolUsed = node.isSSL ? "https" : "http"; @@ -625,8 +640,6 @@ class SolanaWalletClient { incomingAmount = diff.toDouble(); incomingMintAddress = mint; final token = await getTokenInfo(mint); - printV(token?.symbol); - printV(token?.decimals); incomingToken = token ?? const CryptoCurrency(name: "TOKEN", title: "TOKEN", decimals: 6); incomingTo = walletAddress; @@ -951,27 +964,67 @@ class SolanaWalletClient { return mints.toList(); } - /// Load the Address's transactions into the account - Future> fetchTransactions( + Future>> _getAllSignaturesSinceLastFetch( + SolAddress address, + String? until, + Commitment? commitment, + ) async { + final signatures = >[]; + String? before; + + while (true) { + final currentPageSignatureResults = await _provider!.request( + SolanaRPCGetSignaturesForAddress( + account: address, + commitment: commitment, + until: until, + before: before, + limit: _signaturePageSize, + ), + ); + + if (currentPageSignatureResults.isEmpty) break; + + signatures.addAll(currentPageSignatureResults); + + if (currentPageSignatureResults.length < _signaturePageSize) break; + + if (until == null) break; + + final lastSignatureOnPage = currentPageSignatureResults.last['signature'] as String; + + if (lastSignatureOnPage == before) break; + + before = lastSignatureOnPage; + } + + return signatures; + } + + Future fetchTransactions( SolAddress address, { SPLToken? splToken, Commitment? commitment, SolAddress? walletAddress, + String? untilSignature, required void Function(List) onUpdate, }) async { - List transactions = []; + final transactions = []; + try { - final signatures = await _provider!.request( - SolanaRPCGetSignaturesForAddress( - account: address, - commitment: commitment, - ), - ); + final signatures = + await _getAllSignaturesSinceLastFetch(address, untilSignature, commitment); + + if (signatures.isEmpty) return TransactionSyncResult(transactions: transactions); // The maximum concurrent batch size. const int batchSize = 10; + bool hasFailures = false; + for (int i = 0; i < signatures.length; i += batchSize) { + if (_isStopped) return TransactionSyncResult(transactions: transactions); + final batch = signatures.skip(i).take(batchSize).toList(); final batchResponses = await Future.wait(batch.map((signature) async { @@ -985,6 +1038,7 @@ class SolanaWalletClient { ), ); } catch (e) { + hasFailures = true; return null; } })); @@ -999,16 +1053,16 @@ class SolanaWalletClient { final parsedTransactionsLists = await Future.wait(parsedTransactionsFutures); - // We flatten the list of lists into a single list + final batchTransactions = []; for (final parsedList in parsedTransactionsLists) { if (parsedList != null) { - transactions.addAll(parsedList); + batchTransactions.addAll(parsedList); } } - // Only update UI if we have new valid transactions - if (transactions.isNotEmpty) { - onUpdate(List.from(transactions)); + if (batchTransactions.isNotEmpty) { + transactions.addAll(batchTransactions); + onUpdate(batchTransactions); } if (i + batchSize < signatures.length) { @@ -1016,38 +1070,53 @@ class SolanaWalletClient { } } - return transactions; + return TransactionSyncResult( + transactions: transactions, + newestSignature: hasFailures ? null : signatures.first['signature'] as String, + ); } catch (err, s) { printV('Error fetching transactions: $err \n$s'); - return []; + return TransactionSyncResult(transactions: transactions); } } - Future> getSPLTokenTransfers({ + final Map associatedTokenAccountCache = {}; + + Future getSPLTokenTransfers({ required String mintAddress, required SPLToken splToken, required SolanaPrivateKey privateKey, + String? untilSignature, required void Function(List) onUpdate, }) async { - ProgramDerivedAddress? associatedTokenAccount; final ownerWalletAddress = privateKey.publicKey().toAddress(); - try { - associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( - payerPrivateKey: privateKey, - mintAddress: SolAddress(mintAddress), - ownerAddress: ownerWalletAddress, - shouldCreateATA: false, - ); - } catch (e, s) { - printV('$e \n $s'); - } - if (associatedTokenAccount == null) return []; + var associatedTokenAccount = associatedTokenAccountCache[mintAddress]; + + if (associatedTokenAccount == null) { + try { + associatedTokenAccount = await _getOrCreateAssociatedTokenAccount( + payerPrivateKey: privateKey, + mintAddress: SolAddress(mintAddress), + ownerAddress: ownerWalletAddress, + shouldCreateATA: false, + ); + } catch (e, s) { + printV('$e \n $s'); + } + + if (associatedTokenAccount == null) { + return TransactionSyncResult(transactions: []); + } + + associatedTokenAccountCache[mintAddress] = associatedTokenAccount; + } return fetchTransactions( associatedTokenAccount.address, splToken: splToken, walletAddress: ownerWalletAddress, + untilSignature: untilSignature, onUpdate: onUpdate, ); } @@ -1055,16 +1124,9 @@ class SolanaWalletClient { final Map tokenInfoCache = {}; Future getTokenInfo(String mintAddress) async { - if (tokenInfoCache.containsKey(mintAddress)) { - printV("Cached"); - return tokenInfoCache[mintAddress]; - } else { - final token = await fetchSPLTokenInfo(mintAddress); - if (token != null) { - tokenInfoCache[mintAddress] = token; - } - return token; - } + if (tokenInfoCache.containsKey(mintAddress)) return tokenInfoCache[mintAddress]; + + return tokenInfoCache[mintAddress] = await fetchSPLTokenInfo(mintAddress); } Future fetchSPLTokenInfo(String mintAddress) async { @@ -1139,7 +1201,7 @@ class SolanaWalletClient { } } - void stop() {} + void stop() => _isStopped = true; SolanaRPC? get getSolanaProvider => _provider; diff --git a/cw_solana/lib/solana_transaction_history.dart b/cw_solana/lib/solana_transaction_history.dart index 62b9db8f7..00fcbec90 100644 --- a/cw_solana/lib/solana_transaction_history.dart +++ b/cw_solana/lib/solana_transaction_history.dart @@ -31,18 +31,36 @@ abstract class SolanaTransactionHistoryBase extends TransactionHistoryBase _saveQueue = Future.value(); + @override - Future save() async { - try { - final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); - final path = '$dirPath/$transactionsHistoryFileName'; - final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); - final data = json.encode({'transactions': transactionMaps}); - await encryptionFileUtils.write(path: path, password: _password, data: data); - } catch (e, s) { - printV('Error while saving solana transaction history: ${e.toString()}'); - printV(s); - } + Future save() => saveAndConfirm(); + + Future saveAndConfirm() { + final write = _saveQueue.then((_) async { + try { + await _write(); + + return true; + } catch (e, s) { + printV('Error while saving solana transaction history: ${e.toString()}'); + printV(s); + + return false; + } + }); + + _saveQueue = write; + + return write; + } + + Future _write() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + await encryptionFileUtils.write(path: path, password: _password, data: data); } @override diff --git a/cw_solana/lib/solana_wallet.dart b/cw_solana/lib/solana_wallet.dart index 94160686e..2305f27fb 100644 --- a/cw_solana/lib/solana_wallet.dart +++ b/cw_solana/lib/solana_wallet.dart @@ -88,6 +88,8 @@ abstract class SolanaWalletBase Timer? _transactionsUpdateTimer; + Future? _currentRefresh; + late final Box splTokensBox; @override @@ -321,6 +323,33 @@ abstract class SolanaWalletBase ]); } + static const _nativeSource = 'native'; + + String _lastSyncedSignatureKey(String source) => + 'solana_last_synced_signature_${walletInfo.name}_$source'; + + Future _lastSyncedSignature(String source) async { + if (transactionHistory.transactions.isEmpty) return null; + + final prefs = await _sharedPrefs.future; + + return prefs.getString(_lastSyncedSignatureKey(source)); + } + + Future _saveLastSyncedSignature(String source, String? signature) async { + if (signature == null) return; + + final prefs = await _sharedPrefs.future; + + await prefs.setString(_lastSyncedSignatureKey(source), signature); + } + + Future _clearLastSyncedSignature(String source) async { + final prefs = await _sharedPrefs.future; + + await prefs.remove(_lastSyncedSignatureKey(source)); + } + /// Polls for a specific transaction by signature with exponential backoff /// I'm using this in case we make the call to fetch the transaction and it has not finished its confirmations on the solana network and been indexed by the node networks we use. Future pollForTransaction({ @@ -365,20 +394,34 @@ abstract class SolanaWalletBase await updateTransactionsHistory(); } - void updateTransactions(List updatedTx) { - addTransactionsToTransactionHistory(updatedTx); - } + void updateTransactions(List updatedTx) => _addTransactions(updatedTx); /// Fetches the native SOL transactions linked to the wallet Public Key Future _updateNativeSOLTransactions() async { - final transactions = - await _client.fetchTransactions(_solanaPublicKey.toAddress(), onUpdate: updateTransactions); + final result = await _client.fetchTransactions( + _solanaPublicKey.toAddress(), + untilSignature: await _lastSyncedSignature(_nativeSource), + onUpdate: updateTransactions, + ); - await addTransactionsToTransactionHistory(transactions); + await _updateStateWhenSyncForTheSourceEnds(_nativeSource, result); + } + + Future _updateStateWhenSyncForTheSourceEnds( + String source, + TransactionSyncResult result, + ) async { + if (result.transactions.isNotEmpty) { + final isSaved = await transactionHistory.saveAndConfirm(); + + if (!isSaved) return; + } + + await _saveLastSyncedSignature(source, result.newestSignature); } Future updateSPLTokenTransactions({List? specificMints}) async { - final allTokens = balance.keys.whereType().toList(growable: false); + final allTokens = splTokensBox.values.where((t) => t.enabled).toList(growable: false); // Filter to specific mints if provided final tokens = specificMints != null @@ -394,30 +437,28 @@ abstract class SolanaWalletBase i, i + batchSize > tokens.length ? tokens.length : i + batchSize, ); - final results = await Future.wait( + + await Future.wait( batch.map((token) async { try { - return await _client.getSPLTokenTransfers( + final result = await _client.getSPLTokenTransfers( mintAddress: token.mintAddress, splToken: token, privateKey: _solanaPrivateKey, + untilSignature: await _lastSyncedSignature(token.mintAddress), onUpdate: updateTransactions, ); - } catch (_) { - return []; + + await _updateStateWhenSyncForTheSourceEnds(token.mintAddress, result); + } catch (e) { + printV('Error fetching spl token (${token.symbol}) transfers ${e.toString()}'); } }), ); - - for (final list in results) { - await addTransactionsToTransactionHistory(list); - } } } - Future addTransactionsToTransactionHistory( - List transactions, - ) async { + void _addTransactions(List transactions) { final Map result = {}; for (var transactionModel in transactions) { @@ -436,6 +477,12 @@ abstract class SolanaWalletBase } transactionHistory.addMany(result); + } + + Future addTransactionsToTransactionHistory( + List transactions, + ) async { + _addTransactions(transactions); await transactionHistory.save(); } @@ -456,6 +503,17 @@ abstract class SolanaWalletBase await transactionHistory.save(); } + // we want to handle the case where multiple refresh triggers (our users can swipe down + // multiple times), so we track the currrent refresh and join it instead of starting + // another one + Future _refresh() { + return _currentRefresh ??= Future.wait([ + updateTokenBalance(), + updateTransactionsHistory(), + _getEstimatedFees(), + ]).whenComplete(() => _currentRefresh = null); + } + @action @override Future startSync() async { @@ -469,12 +527,7 @@ abstract class SolanaWalletBase return; } - await Future.wait([ - updateTokenBalance(), - _updateNativeSOLTransactions(), - updateSPLTokenTransactions(), - _getEstimatedFees(), - ]); + await _refresh(); syncStatus = SyncedSyncStatus(); } catch (e) { @@ -626,6 +679,14 @@ abstract class SolanaWalletBase List get splTokenCurrencies => splTokensBox.values.toList(); + SPLToken? splTokenBySymbol(String symbol) { + for (final token in splTokensBox.values) { + if (token.symbol == symbol) return token; + } + + return null; + } + void addInitialTokens() { final initialSPLTokens = DefaultSPLTokens().initialSPLTokens; @@ -802,12 +863,27 @@ abstract class SolanaWalletBase } Future deleteSPLToken(SPLToken token) async { + final sources = {token.mintAddress}; + + if (token.symbol == CryptoCurrency.sol.symbol) { + sources.add(_nativeSource); + } + if (splTokensBox.isOpen) { + sources.addAll(splTokensBox.values + .where((t) => t.symbol == token.symbol) + .map((t) => t.mintAddress)); + await splTokensBox.delete(token.mintAddress); } balance.remove(token); await _removeTokenTransactionsInHistory(token); + + for (final source in sources) { + await _clearLastSyncedSignature(source); + } + updateTokenBalance(); } @@ -831,11 +907,12 @@ abstract class SolanaWalletBase _transactionsUpdateTimer!.cancel(); } - _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) { - updateTokenBalance(); - _updateNativeSOLTransactions(); - updateSPLTokenTransactions(); - _getEstimatedFees(); + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) async { + try { + await _refresh(); + } catch (e) { + printV('Error on periodic solana refresh: $e'); + } }); } diff --git a/cw_solana/lib/solana_wallet_service.dart b/cw_solana/lib/solana_wallet_service.dart index e374c9327..1ed55c411 100644 --- a/cw_solana/lib/solana_wallet_service.dart +++ b/cw_solana/lib/solana_wallet_service.dart @@ -13,6 +13,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_solana/solana_mnemonics.dart'; import 'package:cw_solana/solana_wallet.dart'; import 'package:cw_solana/solana_wallet_creation_credentials.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class SolanaWalletService extends WalletService< SolanaNewWalletCredentials, @@ -96,6 +97,11 @@ class SolanaWalletService extends WalletService< throw Exception('Wallet not found'); } await WalletInfo.delete(walletInfo); + final prefs = await SharedPreferences.getInstance(); + for (final key in prefs.getKeys().where( + (k) => k.startsWith('solana_last_synced_signature_${wallet}_'))) { + await prefs.remove(key); + } } @override diff --git a/cw_wownero/lib/api/account_list.dart b/cw_wownero/lib/api/account_list.dart index 5e9edb9f8..dc83be257 100644 --- a/cw_wownero/lib/api/account_list.dart +++ b/cw_wownero/lib/api/account_list.dart @@ -13,7 +13,6 @@ wownero.WalletListener getWlptr() { return _wlptr!; } - wownero.SubaddressAccount? subaddressAccount; bool isUpdating = false; @@ -69,6 +68,6 @@ Future addAccount({required String label}) async { } Future setLabelForAccount({required int accountIndex, required String label}) async { - _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); - await store(); -} \ No newline at end of file + _setLabelForAccount({'accountIndex': accountIndex, 'label': label}); + await store(); +} diff --git a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart index 483b0a174..2ee897e1b 100644 --- a/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart +++ b/cw_wownero/lib/api/exceptions/connection_to_node_exception.dart @@ -2,4 +2,4 @@ class ConnectionToNodeException implements Exception { ConnectionToNodeException({required this.message}); final String message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart b/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart index 7b55ec074..3e7f2abb0 100644 --- a/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart +++ b/cw_wownero/lib/api/exceptions/creation_transaction_exception.dart @@ -1,8 +1,8 @@ class CreationTransactionException implements Exception { CreationTransactionException({required this.message}); - + final String message; @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart index c0d25c62f..4efb63944 100644 --- a/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart +++ b/cw_wownero/lib/api/exceptions/setup_wallet_exception.dart @@ -1,8 +1,8 @@ class SetupWalletException implements Exception { SetupWalletException({required this.message}); - + final String message; @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart b/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart index 6052366b9..8e823dc0c 100644 --- a/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_creation_exception.dart @@ -5,4 +5,4 @@ class WalletCreationException implements Exception { @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart b/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart index df7a850a4..5fb6c6b14 100644 --- a/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_opening_exception.dart @@ -5,4 +5,4 @@ class WalletOpeningException implements Exception { @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart index 6c461ee4c..9c9c5f4b2 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_keys_exception.dart @@ -1,8 +1,8 @@ class WalletRestoreFromKeysException implements Exception { WalletRestoreFromKeysException({required this.message}); - + final String message; @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart b/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart index 004cd7958..6d2ee254e 100644 --- a/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart +++ b/cw_wownero/lib/api/exceptions/wallet_restore_from_seed_exception.dart @@ -1,5 +1,5 @@ class WalletRestoreFromSeedException implements Exception { WalletRestoreFromSeedException({required this.message}); - + final String message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/structs/account_row.dart b/cw_wownero/lib/api/structs/account_row.dart index aa492ee0f..5675e787a 100644 --- a/cw_wownero/lib/api/structs/account_row.dart +++ b/cw_wownero/lib/api/structs/account_row.dart @@ -4,7 +4,7 @@ import 'package:ffi/ffi.dart'; class AccountRow extends Struct { @Int64() external int id; - + external Pointer label; String getLabel() => label.toDartString(); diff --git a/cw_wownero/lib/api/structs/pending_transaction.dart b/cw_wownero/lib/api/structs/pending_transaction.dart index dc5fbddd0..548208f54 100644 --- a/cw_wownero/lib/api/structs/pending_transaction.dart +++ b/cw_wownero/lib/api/structs/pending_transaction.dart @@ -1,12 +1,11 @@ - class PendingTransactionDescription { - PendingTransactionDescription({ - required this.amount, - required this.fee, - required this.hash, - required this.hex, - required this.txKey, - required this.pointerAddress}); + PendingTransactionDescription( + {required this.amount, + required this.fee, + required this.hash, + required this.hex, + required this.txKey, + required this.pointerAddress}); final int amount; final int fee; @@ -14,4 +13,4 @@ class PendingTransactionDescription { final String hex; final String txKey; final int pointerAddress; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/structs/subaddress_row.dart b/cw_wownero/lib/api/structs/subaddress_row.dart index d593a793d..7968ac5aa 100644 --- a/cw_wownero/lib/api/structs/subaddress_row.dart +++ b/cw_wownero/lib/api/structs/subaddress_row.dart @@ -4,12 +4,12 @@ import 'package:ffi/ffi.dart'; class SubaddressRow extends Struct { @Int64() external int id; - + external Pointer address; - + external Pointer label; String getLabel() => label.toDartString(); String getAddress() => address.toDartString(); int getId() => id; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/api/subaddress_list.dart b/cw_wownero/lib/api/subaddress_list.dart index b07f24c4a..ad54175c9 100644 --- a/cw_wownero/lib/api/subaddress_list.dart +++ b/cw_wownero/lib/api/subaddress_list.dart @@ -38,7 +38,8 @@ class Subaddress { ); final int addressIndex; final int accountIndex; - String get label => wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + String get label => wownero.Wallet_getSubaddressLabel(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); final int txCount; final int received; } @@ -68,7 +69,10 @@ List getAllSubaddresses() { final subaddrs = wownero.TransactionInfo_subaddrIndex(tx).split(","); final account = wownero.TransactionInfo_subaddrAccount(tx); newttDetails.add(TinyTransactionDetails( - address: List.generate(subaddrs.length, (index) => getAddress(accountIndex: account, addressIndex: int.tryParse(subaddrs[index])??0)), + address: List.generate( + subaddrs.length, + (index) => getAddress( + accountIndex: account, addressIndex: int.tryParse(subaddrs[index]) ?? 0)), amount: wownero.TransactionInfo_amount(tx), )); } @@ -79,7 +83,7 @@ List getAllSubaddresses() { final list = List.generate(size, (index) { final ttDetailsLocal = ttDetails.where((element) { final address = getAddress( - accountIndex: subaddress!.accountIndex, + accountIndex: subaddress!.accountIndex, addressIndex: index, ); if (address == element.address) return true; @@ -97,7 +101,8 @@ List getAllSubaddresses() { ); }).reversed.toList(); if (list.isEmpty) { - list.add(Subaddress(addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); + list.add(Subaddress( + addressIndex: 0, accountIndex: subaddress!.accountIndex, txCount: 0, received: 0)); } return list; } @@ -113,7 +118,8 @@ int numSubaddresses(int subaccountIndex) { void setLabelForSubaddressSync( {required int accountIndex, required int addressIndex, required String label}) { - wownero.Wallet_setSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex, label: label); + wownero.Wallet_setSubaddressLabel(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex, label: label); } void _addSubaddress(Map args) { @@ -128,8 +134,7 @@ void _setLabelForSubaddress(Map args) { final accountIndex = args['accountIndex'] as int; final addressIndex = args['addressIndex'] as int; - setLabelForSubaddressSync( - accountIndex: accountIndex, addressIndex: addressIndex, label: label); + setLabelForSubaddressSync(accountIndex: accountIndex, addressIndex: addressIndex, label: label); } Future addSubaddress({required int accountIndex, required String label}) async { @@ -138,11 +143,8 @@ Future addSubaddress({required int accountIndex, required String label}) a } Future setLabelForSubaddress( - {required int accountIndex, required int addressIndex, required String label}) async { - _setLabelForSubaddress({ - 'accountIndex': accountIndex, - 'addressIndex': addressIndex, - 'label': label - }); + {required int accountIndex, required int addressIndex, required String label}) async { + _setLabelForSubaddress( + {'accountIndex': accountIndex, 'addressIndex': addressIndex, 'label': label}); await store(); } diff --git a/cw_wownero/lib/api/transaction_history.dart b/cw_wownero/lib/api/transaction_history.dart index 10bfb535e..13dae3d71 100644 --- a/cw_wownero/lib/api/transaction_history.dart +++ b/cw_wownero/lib/api/transaction_history.dart @@ -43,33 +43,36 @@ Future> getAllTransactions() async { await txHistoryMutex.acquire(); txhistory ??= wownero.Wallet_history(wptr!); int size = countOfTransactions(); - final list = List.generate(size, (index) => Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index))); + final list = List.generate( + size, + (index) => + Transaction(txInfo: wownero.TransactionHistory_transaction(txhistory!, index: index))); txHistoryMutex.release(); - + final accts = wownero.Wallet_numSubaddressAccounts(wptr!); - for (var i = 0; i < accts; i++) { + for (var i = 0; i < accts; i++) { final fullBalance = wownero.Wallet_balance(wptr!, accountIndex: i); final availBalance = wownero.Wallet_unlockedBalance(wptr!, accountIndex: i); if (fullBalance > availBalance) { - if (list.where((element) => element.accountIndex == i && element.isConfirmed == false).isEmpty) { - dummyTxs.add( - Transaction.dummy( - displayLabel: "", - description: "", - fee: 0, - confirmations: 0, - blockheight: 0, - accountIndex: i, - addressIndex: 0, - addressIndexList: [0], - paymentId: "", - amount: fullBalance - availBalance, - isSpend: false, - hash: "pending", - key: "pending", - txInfo: Pointer.fromAddress(0), - )..timeStamp = DateTime.now() - ); + if (list + .where((element) => element.accountIndex == i && element.isConfirmed == false) + .isEmpty) { + dummyTxs.add(Transaction.dummy( + displayLabel: "", + description: "", + fee: 0, + confirmations: 0, + blockheight: 0, + accountIndex: i, + addressIndex: 0, + addressIndexList: [0], + paymentId: "", + amount: fullBalance - availBalance, + isSpend: false, + hash: "pending", + key: "pending", + txInfo: Pointer.fromAddress(0), + )..timeStamp = DateTime.now()); } } } @@ -89,13 +92,13 @@ Future createTransactionSync( String? amount, int accountIndex = 0, List preferredInputs = const []}) async { - final amt = amount == null ? 0 : wownero.Wallet_amountFromString(amount); - - final address_ = address.toNativeUtf8(); + + final address_ = address.toNativeUtf8(); final paymentId_ = paymentId.toNativeUtf8(); if (preferredInputs.isEmpty) { - throw WowneroTransactionCreationException("No inputs provided, transaction cannot be constructed"); + throw WowneroTransactionCreationException( + "No inputs provided, transaction cannot be constructed"); } final preferredInputs_ = preferredInputs.join(wownero.defaultSeparatorStr).toNativeUtf8(); @@ -105,7 +108,8 @@ Future createTransactionSync( final preferredInputsAddr = preferredInputs_.address; final spaddr = wownero.defaultSeparator.address; final pendingTx = Pointer.fromAddress(await Isolate.run(() { - final tx = wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction( + final tx = + wownero_gen.WowneroC(DynamicLibrary.open(wownero.libPath)).WOWNERO_Wallet_createTransaction( Pointer.fromAddress(waddr), Pointer.fromAddress(addraddr).cast(), Pointer.fromAddress(paymentIdAddr).cast(), @@ -140,13 +144,13 @@ Future createTransactionSync( final rTxKey = rHash; return PendingTransactionDescription( - amount: rAmt, - fee: rFee, - hash: rHash, - hex: '', - txKey: rTxKey, - pointerAddress: pendingTx.address, - ); + amount: rAmt, + fee: rFee, + hash: rHash, + hex: '', + txKey: rTxKey, + pointerAddress: pendingTx.address, + ); } PendingTransactionDescription createTransactionMultDestSync( @@ -155,7 +159,6 @@ PendingTransactionDescription createTransactionMultDestSync( required int priorityRaw, int accountIndex = 0, List preferredInputs = const []}) { - final txptr = wownero.Wallet_createTransactionMultDest( wptr!, dstAddr: outputs.map((e) => e.address).toList(), @@ -182,8 +185,8 @@ void commitTransactionFromPointerAddress({required int address}) => commitTransaction(transactionPointer: wownero.PendingTransaction.fromAddress(address)); void commitTransaction({required wownero.PendingTransaction transactionPointer}) { - - final txCommit = wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); + final txCommit = + wownero.PendingTransaction_commit(transactionPointer, filename: '', overwrite: false); String? error = (() { final status = wownero.PendingTransaction_status(transactionPointer.cast()); @@ -200,9 +203,8 @@ void commitTransaction({required wownero.PendingTransaction transactionPointer}) } return wownero.Wallet_errorString(wptr!); })(); - } - + if (error != null) { throw CreationTransactionException(message: error); } @@ -270,19 +272,20 @@ Future createTransactionMultDest( 'preferredInputs': preferredInputs }); - class Transaction { final String displayLabel; - late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); + late final String subaddressLabel = wownero.Wallet_getSubaddressLabel(wptr!, + accountIndex: accountIndex, addressIndex: addressIndex); late final String address = getAddress( accountIndex: accountIndex, addressIndex: addressIndex, ); - late final List addressList = List.generate(addressIndexList.length, (index) => - getAddress( - accountIndex: accountIndex, - addressIndex: addressIndexList[index], - )); + late final List addressList = List.generate( + addressIndexList.length, + (index) => getAddress( + accountIndex: accountIndex, + addressIndex: addressIndexList[index], + )); final String description; final int fee; final int confirmations; @@ -331,33 +334,36 @@ class Transaction { timeStamp = DateTime.fromMillisecondsSinceEpoch( wownero.TransactionInfo_timestamp(txInfo) * 1000, ), - isSpend = wownero.TransactionInfo_direction(txInfo) == - wownero.TransactionInfo_Direction.Out, + isSpend = + wownero.TransactionInfo_direction(txInfo) == wownero.TransactionInfo_Direction.Out, amount = wownero.TransactionInfo_amount(txInfo), paymentId = wownero.TransactionInfo_paymentId(txInfo), accountIndex = wownero.TransactionInfo_subaddrAccount(txInfo), - addressIndex = int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, - addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo).split(", ").map((e) => int.tryParse(e) ?? 0).toList(), + addressIndex = + int.tryParse(wownero.TransactionInfo_subaddrIndex(txInfo).split(", ")[0]) ?? 0, + addressIndexList = wownero.TransactionInfo_subaddrIndex(txInfo) + .split(", ") + .map((e) => int.tryParse(e) ?? 0) + .toList(), blockheight = wownero.TransactionInfo_blockHeight(txInfo), confirmations = wownero.TransactionInfo_confirmations(txInfo), fee = wownero.TransactionInfo_fee(txInfo), description = wownero.TransactionInfo_description(txInfo), key = wownero.Wallet_getTxKey(wptr!, txid: wownero.TransactionInfo_hash(txInfo)); - Transaction.dummy({ - required this.displayLabel, - required this.description, - required this.fee, - required this.confirmations, - required this.blockheight, - required this.accountIndex, - required this.addressIndex, - required this.addressIndexList, - required this.paymentId, - required this.amount, - required this.isSpend, - required this.hash, - required this.key, - required this.txInfo - }); + Transaction.dummy( + {required this.displayLabel, + required this.description, + required this.fee, + required this.confirmations, + required this.blockheight, + required this.accountIndex, + required this.addressIndex, + required this.addressIndexList, + required this.paymentId, + required this.amount, + required this.isSpend, + required this.hash, + required this.key, + required this.txInfo}); } diff --git a/cw_wownero/lib/api/wallet.dart b/cw_wownero/lib/api/wallet.dart index 68d0b654b..c9edf2273 100644 --- a/cw_wownero/lib/api/wallet.dart +++ b/cw_wownero/lib/api/wallet.dart @@ -34,8 +34,7 @@ String getFilename() => wownero.Wallet_filename(wptr!); String getSeed() { // monero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - final cakepolyseed = - wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); + final cakepolyseed = wownero.Wallet_getCacheAttribute(wptr!, key: "cakewallet.seed"); final cakepassphrase = getPassphrase(); final weirdPolyseed = wownero.Wallet_getPolyseed(wptr!, passphrase: cakepassphrase); @@ -65,11 +64,21 @@ String getSeedLegacy(String? language) { final cakepassphrase = getPassphrase(); var legacy = wownero.Wallet_seed(wptr!, seedOffset: cakepassphrase); switch (language) { - case "Chinese (Traditional)": language = "Chinese (simplified)"; break; - case "Chinese (Simplified)": language = "Chinese (simplified)"; break; - case "Korean": language = "English"; break; - case "Czech": language = "English"; break; - case "Japanese": language = "English"; break; + case "Chinese (Traditional)": + language = "Chinese (simplified)"; + break; + case "Chinese (Simplified)": + language = "Chinese (simplified)"; + break; + case "Korean": + language = "English"; + break; + case "Czech": + language = "English"; + break; + case "Japanese": + language = "English"; + break; } if (wownero.Wallet_status(wptr!) != 0) { wownero.Wallet_setSeedLanguage(wptr!, language: language ?? "English"); @@ -84,6 +93,7 @@ String getSeedLegacy(String? language) { } return legacy; } + Map>> addressCache = {}; String getPassphrase() { @@ -91,14 +101,14 @@ String getPassphrase() { } String getAddress({int accountIndex = 0, int addressIndex = 1}) { - while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex)-1 < addressIndex) { + while (wownero.Wallet_numSubaddresses(wptr!, accountIndex: accountIndex) - 1 < addressIndex) { printV("adding subaddress"); wownero.Wallet_addSubaddress(wptr!, accountIndex: accountIndex); } addressCache[wptr!.address] ??= {}; addressCache[wptr!.address]![accountIndex] ??= {}; - addressCache[wptr!.address]![accountIndex]![addressIndex] ??= wownero.Wallet_address(wptr!, - accountIndex: accountIndex, addressIndex: addressIndex); + addressCache[wptr!.address]![accountIndex]![addressIndex] ??= + wownero.Wallet_address(wptr!, accountIndex: accountIndex, addressIndex: addressIndex); return addressCache[wptr!.address]![accountIndex]![addressIndex]!; } @@ -106,10 +116,12 @@ int getFullBalance({int accountIndex = 0}) { if (wptr == null) return 0; return wownero.Wallet_balance(wptr!, accountIndex: accountIndex); } + int getUnlockedBalance({int accountIndex = 0}) { if (wptr == null) return 0; return wownero.Wallet_unlockedBalance(wptr!, accountIndex: accountIndex); } + int getCurrentHeight() { if (wptr == null) return 0; return wownero.Wallet_blockChainHeight(wptr!); @@ -181,8 +193,7 @@ Future connectToNode() async { } void setRefreshFromBlockHeight({required int height}) => - wownero.Wallet_setRefreshFromBlockHeight(wptr!, - refresh_from_block_height: height); + wownero.Wallet_setRefreshFromBlockHeight(wptr!, refresh_from_block_height: height); void setRecoveringFromSeed({required bool isRecovery}) => wownero.Wallet_setRecoveringFromSeed(wptr!, recoveringFromSeed: isRecovery); @@ -257,8 +268,7 @@ class SyncListener { _cachedBlockchainHeight = 0; _lastKnownBlockHeight = 0; _initialSyncHeight = 0; - _updateSyncInfoTimer ??= - Timer.periodic(Duration(milliseconds: 1200), (_) async { + _updateSyncInfoTimer ??= Timer.periodic(Duration(milliseconds: 1200), (_) async { if (isNewTransactionExist()) { onNewTransaction(); } @@ -297,8 +307,8 @@ class SyncListener { void stop() => _updateSyncInfoTimer?.cancel(); } -SyncListener setListeners(void Function(int, int, double) onNewBlock, - void Function() onNewTransaction) { +SyncListener setListeners( + void Function(int, int, double) onNewBlock, void Function() onNewTransaction) { final listener = SyncListener(onNewBlock, onNewTransaction); // setListenerNative(); return listener; @@ -360,8 +370,7 @@ String getSubaddressLabel(int accountIndex, int addressIndex) { accountIndex: accountIndex, addressIndex: addressIndex); } -Future setTrustedDaemon(bool trusted) async => - wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted); +Future setTrustedDaemon(bool trusted) async => wownero.Wallet_setTrustedDaemon(wptr!, arg: trusted); Future trustedDaemon() async => wownero.Wallet_trustedDaemon(wptr!); @@ -370,7 +379,8 @@ String signMessage(String message, {String address = ""}) { } bool verifyMessage(String message, String address, String signature) { - return wownero.Wallet_verifySignedMessage(wptr!, message: message, address: address, signature: signature); + return wownero.Wallet_verifySignedMessage(wptr!, + message: message, address: address, signature: signature); } -Map> debugCallLength() => wownero.debugCallLength; \ No newline at end of file +Map> debugCallLength() => wownero.debugCallLength; diff --git a/cw_wownero/lib/api/wallet_manager.dart b/cw_wownero/lib/api/wallet_manager.dart index 060ebd4d0..d9632048c 100644 --- a/cw_wownero/lib/api/wallet_manager.dart +++ b/cw_wownero/lib/api/wallet_manager.dart @@ -34,15 +34,18 @@ void checkIfMoneroCIsFine() { final dartCsExp = wownero.wallet2_api_c_exp_sha256; if (cppCsCpp != dartCsCpp) { - throw MoneroCException("monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'"); + throw MoneroCException( + "monero_c and monero.dart cpp wrapper code mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsCpp'\ndart: '$dartCsCpp'"); } if (cppCsH != dartCsH) { - throw MoneroCException("monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'"); + throw MoneroCException( + "monero_c and monero.dart cpp wrapper header mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsH'\ndart: '$dartCsH'"); } if (cppCsExp != dartCsExp && (Platform.isIOS || Platform.isMacOS)) { - throw MoneroCException("monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); + throw MoneroCException( + "monero_c and monero.dart wrapper export list mismatch.\nLogic errors can occur.\nRefusing to run in release mode.\ncpp: '$cppCsExp'\ndart: '$dartCsExp'"); } } @@ -133,10 +136,10 @@ void restoreWalletFromSeedSync( } wptr = newWptr; - + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase); wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - + openedWalletsByPath[path] = wptr!; store(); @@ -153,29 +156,27 @@ void restoreWalletFromKeysSync( int restoreHeight = 0}) { txhistory = null; var newWptr = (spendKey != "") - ? wownero.WalletManager_createDeterministicWalletFromSpendKey( - wmPtr, - path: path, - password: password, - language: language, - spendKeyString: spendKey, - newWallet: true, // TODO(mrcyjanek): safe to remove - restoreHeight: restoreHeight) - : wownero.WalletManager_createWalletFromKeys( - wmPtr, - path: path, - password: password, - restoreHeight: restoreHeight, - addressString: address, - viewKeyString: viewKey, - spendKeyString: spendKey, - nettype: 0, - ); + ? wownero.WalletManager_createDeterministicWalletFromSpendKey(wmPtr, + path: path, + password: password, + language: language, + spendKeyString: spendKey, + newWallet: true, // TODO(mrcyjanek): safe to remove + restoreHeight: restoreHeight) + : wownero.WalletManager_createWalletFromKeys( + wmPtr, + path: path, + password: password, + restoreHeight: restoreHeight, + addressString: address, + viewKeyString: viewKey, + spendKeyString: spendKey, + nettype: 0, + ); final status = wownero.Wallet_status(newWptr); if (status != 0) { - throw WalletRestoreFromKeysException( - message: wownero.Wallet_errorString(newWptr)); + throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr)); } // CW-712 - Try to restore deterministic wallet first, if the view key doesn't // match the view key provided @@ -184,7 +185,7 @@ void restoreWalletFromKeysSync( if (viewKey != viewKeyRestored && viewKey != "") { wownero.WalletManager_closeWallet(wmPtr, newWptr, false); File(path).deleteSync(); - File(path+".keys").deleteSync(); + File(path + ".keys").deleteSync(); newWptr = wownero.WalletManager_createWalletFromKeys( wmPtr, path: path, @@ -197,8 +198,7 @@ void restoreWalletFromKeysSync( ); final status = wownero.Wallet_status(newWptr); if (status != 0) { - throw WalletRestoreFromKeysException( - message: wownero.Wallet_errorString(newWptr)); + throw WalletRestoreFromKeysException(message: wownero.Wallet_errorString(newWptr)); } } } @@ -207,8 +207,6 @@ void restoreWalletFromKeysSync( openedWalletsByPath[path] = wptr!; } - - // English only, because normalization. void restoreWalletFromPolyseedWithOffset( {required String path, @@ -217,7 +215,6 @@ void restoreWalletFromPolyseedWithOffset( required String seedOffset, required String language, int nettype = 0}) { - txhistory = null; final newWptr = wownero.WalletManager_createWalletFromPolyseed( wmPtr, @@ -332,8 +329,7 @@ String _lastOpenedWallet = ""; Map openedWalletsByPath = {}; -void loadWallet( - {required String path, required String password, int nettype = 0}) { +void loadWallet({required String path, required String password, int nettype = 0}) { if (openedWalletsByPath[path] != null) { txhistory = null; wptr = openedWalletsByPath[path]!; @@ -347,8 +343,7 @@ void loadWallet( }); } txhistory = null; - final newWptr = wownero.WalletManager_openWallet(wmPtr, - path: path, password: password); + final newWptr = wownero.WalletManager_openWallet(wmPtr, path: path, password: password); _lastOpenedWallet = path; final status = wownero.Wallet_status(newWptr); @@ -379,7 +374,11 @@ void _restoreFromSeed(Map args) { final restoreHeight = args['restoreHeight'] as int; restoreWalletFromSeedSync( - path: path, password: password, passphrase: passphrase, seed: seed, restoreHeight: restoreHeight); + path: path, + password: password, + passphrase: passphrase, + seed: seed, + restoreHeight: restoreHeight); } void _restoreFromKeys(Map args) { @@ -418,19 +417,15 @@ void _restoreFromSpendKey(Map args) { spendKey: spendKey); } -Future _openWallet(Map args) async => loadWallet( - path: args['path'] as String, password: args['password'] as String); +Future _openWallet(Map args) async => + loadWallet(path: args['path'] as String, password: args['password'] as String); Future _isWalletExist(String path) async => isWalletExistSync(path: path); -void openWallet( - {required String path, - required String password, - int nettype = 0}) async => +void openWallet({required String path, required String password, int nettype = 0}) async => loadWallet(path: path, password: password, nettype: nettype); -Future openWalletAsync(Map args) async => - _openWallet(args); +Future openWalletAsync(Map args) async => _openWallet(args); Future createWallet( {required String path, diff --git a/cw_wownero/lib/api/wownero_output.dart b/cw_wownero/lib/api/wownero_output.dart index a4756c4c5..81d1868b1 100644 --- a/cw_wownero/lib/api/wownero_output.dart +++ b/cw_wownero/lib/api/wownero_output.dart @@ -3,4 +3,4 @@ class WowneroOutput { final String address; final String amount; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/cw_wownero.dart b/cw_wownero/lib/cw_wownero.dart index 33a55e305..be989a9ef 100644 --- a/cw_wownero/lib/cw_wownero.dart +++ b/cw_wownero/lib/cw_wownero.dart @@ -1,4 +1,3 @@ - import 'cw_wownero_platform_interface.dart'; class CwWownero { diff --git a/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart b/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart index 3b24be87b..74c8524a8 100644 --- a/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart +++ b/cw_wownero/lib/exceptions/wownero_transaction_creation_exception.dart @@ -5,4 +5,4 @@ class WowneroTransactionCreationException implements Exception { @override String toString() => message; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart b/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart index e42344abd..79fb9fc64 100644 --- a/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart +++ b/cw_wownero/lib/exceptions/wownero_transaction_no_inputs_exception.dart @@ -4,5 +4,6 @@ class WowneroTransactionNoInputsException implements Exception { int inputsSize; @override - String toString() => 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control'; + String toString() => + 'Not enough inputs ($inputsSize) selected. Please select more under Coin Control'; } diff --git a/cw_wownero/lib/mnemonics/chinese_simplified.dart b/cw_wownero/lib/mnemonics/chinese_simplified.dart index da3225041..43595158c 100644 --- a/cw_wownero/lib/mnemonics/chinese_simplified.dart +++ b/cw_wownero/lib/mnemonics/chinese_simplified.dart @@ -1627,4 +1627,4 @@ class ChineseSimplifiedMnemonics { "浙", "貌" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/dutch.dart b/cw_wownero/lib/mnemonics/dutch.dart index 3a1d00cfc..b063edf8b 100644 --- a/cw_wownero/lib/mnemonics/dutch.dart +++ b/cw_wownero/lib/mnemonics/dutch.dart @@ -1627,4 +1627,4 @@ class DutchMnemonics { "zwijmel", "zworen" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/french.dart b/cw_wownero/lib/mnemonics/french.dart index 76d556f6a..aa146d15b 100644 --- a/cw_wownero/lib/mnemonics/french.dart +++ b/cw_wownero/lib/mnemonics/french.dart @@ -1,1630 +1,1630 @@ class FrenchMnemonics { - static const words = [ - "abandon", - "abattre", - "aboi", - "abolir", - "aborder", - "abri", - "absence", - "absolu", - "abuser", - "acacia", - "acajou", - "accent", - "accord", - "accrocher", - "accuser", - "acerbe", - "achat", - "acheter", - "acide", - "acier", - "acquis", - "acte", - "action", - "adage", - "adepte", - "adieu", - "admettre", - "admis", - "adorer", - "adresser", - "aduler", - "affaire", - "affirmer", - "afin", - "agacer", - "agent", - "agir", - "agiter", - "agonie", - "agrafe", - "agrume", - "aider", - "aigle", - "aigre", - "aile", - "ailleurs", - "aimant", - "aimer", - "ainsi", - "aise", - "ajouter", - "alarme", - "album", - "alcool", - "alerte", - "algue", - "alibi", - "aller", - "allumer", - "alors", - "amande", - "amener", - "amie", - "amorcer", - "amour", - "ample", - "amuser", - "ananas", - "ancien", - "anglais", - "angoisse", - "animal", - "anneau", - "annoncer", - "apercevoir", - "apparence", - "appel", - "apporter", - "apprendre", - "appuyer", - "arbre", - "arcade", - "arceau", - "arche", - "ardeur", - "argent", - "argile", - "aride", - "arme", - "armure", - "arracher", - "arriver", - "article", - "asile", - "aspect", - "assaut", - "assez", - "assister", - "assurer", - "astre", - "astuce", - "atlas", - "atroce", - "attacher", - "attente", - "attirer", - "aube", - "aucun", - "audace", - "auparavant", - "auquel", - "aurore", - "aussi", - "autant", - "auteur", - "autoroute", - "autre", - "aval", - "avant", - "avec", - "avenir", - "averse", - "aveu", - "avide", - "avion", - "avis", - "avoir", - "avouer", - "avril", - "azote", - "azur", - "badge", - "bagage", - "bague", - "bain", - "baisser", - "balai", - "balcon", - "balise", - "balle", - "bambou", - "banane", - "banc", - "bandage", - "banjo", - "banlieue", - "bannir", - "banque", - "baobab", - "barbe", - "barque", - "barrer", - "bassine", - "bataille", - "bateau", - "battre", - "baver", - "bavoir", - "bazar", - "beau", - "beige", - "berger", - "besoin", - "beurre", - "biais", - "biceps", - "bidule", - "bien", - "bijou", - "bilan", - "billet", - "blanc", - "blason", - "bleu", - "bloc", - "blond", - "bocal", - "boire", - "boiserie", - "boiter", - "bonbon", - "bondir", - "bonheur", - "bordure", - "borgne", - "borner", - "bosse", - "bouche", - "bouder", - "bouger", - "boule", - "bourse", - "bout", - "boxe", - "brader", - "braise", - "branche", - "braquer", - "bras", - "brave", - "brebis", - "brevet", - "brider", - "briller", - "brin", - "brique", - "briser", - "broche", - "broder", - "bronze", - "brosser", - "brouter", - "bruit", - "brute", - "budget", - "buffet", - "bulle", - "bureau", - "buriner", - "buste", - "buter", - "butiner", - "cabas", - "cabinet", - "cabri", - "cacao", - "cacher", - "cadeau", - "cadre", - "cage", - "caisse", - "caler", - "calme", - "camarade", - "camion", - "campagne", - "canal", - "canif", - "capable", - "capot", - "carat", - "caresser", - "carie", - "carpe", - "cartel", - "casier", - "casque", - "casserole", - "cause", - "cavale", - "cave", - "ceci", - "cela", - "celui", - "cendre", - "cent", - "cependant", - "cercle", - "cerise", - "cerner", - "certes", - "cerveau", - "cesser", - "chacun", - "chair", - "chaleur", - "chamois", - "chanson", - "chaque", - "charge", - "chasse", - "chat", - "chaud", - "chef", - "chemin", - "cheveu", - "chez", - "chicane", - "chien", - "chiffre", - "chiner", - "chiot", - "chlore", - "choc", - "choix", - "chose", - "chou", - "chute", - "cibler", - "cidre", - "ciel", - "cigale", - "cinq", - "cintre", - "cirage", - "cirque", - "ciseau", - "citation", - "citer", - "citron", - "civet", - "clairon", - "clan", - "classe", - "clavier", - "clef", - "climat", - "cloche", - "cloner", - "clore", - "clos", - "clou", - "club", - "cobra", - "cocon", - "coiffer", - "coin", - "colline", - "colon", - "combat", - "comme", - "compte", - "conclure", - "conduire", - "confier", - "connu", - "conseil", - "contre", - "convenir", - "copier", - "cordial", - "cornet", - "corps", - "cosmos", - "coton", - "couche", - "coude", - "couler", - "coupure", - "cour", - "couteau", - "couvrir", - "crabe", - "crainte", - "crampe", - "cran", - "creuser", - "crever", - "crier", - "crime", - "crin", - "crise", - "crochet", - "croix", - "cruel", - "cuisine", - "cuite", - "culot", - "culte", - "cumul", - "cure", - "curieux", - "cuve", - "dame", - "danger", - "dans", - "davantage", - "debout", - "dedans", - "dehors", - "delta", - "demain", - "demeurer", - "demi", - "dense", - "dent", - "depuis", - "dernier", - "descendre", - "dessus", - "destin", - "dette", - "deuil", - "deux", - "devant", - "devenir", - "devin", - "devoir", - "dicton", - "dieu", - "difficile", - "digestion", - "digue", - "diluer", - "dimanche", - "dinde", - "diode", - "dire", - "diriger", - "discours", - "disposer", - "distance", - "divan", - "divers", - "docile", - "docteur", - "dodu", - "dogme", - "doigt", - "dominer", - "donation", - "donjon", - "donner", - "dopage", - "dorer", - "dormir", - "doseur", - "douane", - "double", - "douche", - "douleur", - "doute", - "doux", - "douzaine", - "draguer", - "drame", - "drap", - "dresser", - "droit", - "duel", - "dune", - "duper", - "durant", - "durcir", - "durer", - "eaux", - "effacer", - "effet", - "effort", - "effrayant", - "elle", - "embrasser", - "emmener", - "emparer", - "empire", - "employer", - "emporter", - "enclos", - "encore", - "endive", - "endormir", - "endroit", - "enduit", - "enfant", - "enfermer", - "enfin", - "enfler", - "enfoncer", - "enfuir", - "engager", - "engin", - "enjeu", - "enlever", - "ennemi", - "ennui", - "ensemble", - "ensuite", - "entamer", - "entendre", - "entier", - "entourer", - "entre", - "envelopper", - "envie", - "envoyer", - "erreur", - "escalier", - "espace", - "espoir", - "esprit", - "essai", - "essor", - "essuyer", - "estimer", - "exact", - "examiner", - "excuse", - "exemple", - "exiger", - "exil", - "exister", - "exode", - "expliquer", - "exposer", - "exprimer", - "extase", - "fable", - "facette", - "facile", - "fade", - "faible", - "faim", - "faire", - "fait", - "falloir", - "famille", - "faner", - "farce", - "farine", - "fatigue", - "faucon", - "faune", - "faute", - "faux", - "faveur", - "favori", - "faxer", - "feinter", - "femme", - "fendre", - "fente", - "ferme", - "festin", - "feuille", - "feutre", - "fiable", - "fibre", - "ficher", - "fier", - "figer", - "figure", - "filet", - "fille", - "filmer", - "fils", - "filtre", - "final", - "finesse", - "finir", - "fiole", - "firme", - "fixe", - "flacon", - "flair", - "flamme", - "flan", - "flaque", - "fleur", - "flocon", - "flore", - "flot", - "flou", - "fluide", - "fluor", - "flux", - "focus", - "foin", - "foire", - "foison", - "folie", - "fonction", - "fondre", - "fonte", - "force", - "forer", - "forger", - "forme", - "fort", - "fosse", - "fouet", - "fouine", - "foule", - "four", - "foyer", - "frais", - "franc", - "frapper", - "freiner", - "frimer", - "friser", - "frite", - "froid", - "froncer", - "fruit", - "fugue", - "fuir", - "fuite", - "fumer", - "fureur", - "furieux", - "fuser", - "fusil", - "futile", - "futur", - "gagner", - "gain", - "gala", - "galet", - "galop", - "gamme", - "gant", - "garage", - "garde", - "garer", - "gauche", - "gaufre", - "gaule", - "gaver", - "gazon", - "geler", - "genou", - "genre", - "gens", - "gercer", - "germer", - "geste", - "gibier", - "gicler", - "gilet", - "girafe", - "givre", - "glace", - "glisser", - "globe", - "gloire", - "gluant", - "gober", - "golf", - "gommer", - "gorge", - "gosier", - "goutte", - "grain", - "gramme", - "grand", - "gras", - "grave", - "gredin", - "griffure", - "griller", - "gris", - "gronder", - "gros", - "grotte", - "groupe", - "grue", - "guerrier", - "guetter", - "guider", - "guise", - "habiter", - "hache", - "haie", - "haine", - "halte", - "hamac", - "hanche", - "hangar", - "hanter", - "haras", - "hareng", - "harpe", - "hasard", - "hausse", - "haut", - "havre", - "herbe", - "heure", - "hibou", - "hier", - "histoire", - "hiver", - "hochet", - "homme", - "honneur", - "honte", - "horde", - "horizon", - "hormone", - "houle", - "housse", - "hublot", - "huile", - "huit", - "humain", - "humble", - "humide", - "humour", - "hurler", - "idole", - "igloo", - "ignorer", - "illusion", - "image", - "immense", - "immobile", - "imposer", - "impression", - "incapable", - "inconnu", - "index", - "indiquer", - "infime", - "injure", - "inox", - "inspirer", - "instant", - "intention", - "intime", - "inutile", - "inventer", - "inviter", - "iode", - "iris", - "issue", - "ivre", - "jade", - "jadis", - "jamais", - "jambe", - "janvier", - "jardin", - "jauge", - "jaunisse", - "jeter", - "jeton", - "jeudi", - "jeune", - "joie", - "joindre", - "joli", - "joueur", - "journal", - "judo", - "juge", - "juillet", - "juin", - "jument", - "jungle", - "jupe", - "jupon", - "jurer", - "juron", - "jury", - "jusque", - "juste", - "kayak", - "ketchup", - "kilo", - "kiwi", - "koala", - "label", - "lacet", - "lacune", - "laine", - "laisse", - "lait", - "lame", - "lancer", - "lande", - "laque", - "lard", - "largeur", - "larme", - "larve", - "lasso", - "laver", - "lendemain", - "lentement", - "lequel", - "lettre", - "leur", - "lever", - "levure", - "liane", - "libre", - "lien", - "lier", - "lieutenant", - "ligne", - "ligoter", - "liguer", - "limace", - "limer", - "limite", - "lingot", - "lion", - "lire", - "lisser", - "litre", - "livre", - "lobe", - "local", - "logis", - "loin", - "loisir", - "long", - "loque", - "lors", - "lotus", - "louer", - "loup", - "lourd", - "louve", - "loyer", - "lubie", - "lucide", - "lueur", - "luge", - "luire", - "lundi", - "lune", - "lustre", - "lutin", - "lutte", - "luxe", - "machine", - "madame", - "magie", - "magnifique", - "magot", - "maigre", - "main", - "mairie", - "maison", - "malade", - "malheur", - "malin", - "manche", - "manger", - "manier", - "manoir", - "manquer", - "marche", - "mardi", - "marge", - "mariage", - "marquer", - "mars", - "masque", - "masse", - "matin", - "mauvais", - "meilleur", - "melon", - "membre", - "menacer", - "mener", - "mensonge", - "mentir", - "menu", - "merci", - "merlu", - "mesure", - "mettre", - "meuble", - "meunier", - "meute", - "miche", - "micro", - "midi", - "miel", - "miette", - "mieux", - "milieu", - "mille", - "mimer", - "mince", - "mineur", - "ministre", - "minute", - "mirage", - "miroir", - "miser", - "mite", - "mixte", - "mobile", - "mode", - "module", - "moins", - "mois", - "moment", - "momie", - "monde", - "monsieur", - "monter", - "moquer", - "moral", - "morceau", - "mordre", - "morose", - "morse", - "mortier", - "morue", - "motif", - "motte", - "moudre", - "moule", - "mourir", - "mousse", - "mouton", - "mouvement", - "moyen", - "muer", - "muette", - "mugir", - "muguet", - "mulot", - "multiple", - "munir", - "muret", - "muse", - "musique", - "muter", - "nacre", - "nager", - "nain", - "naissance", - "narine", - "narrer", - "naseau", - "nasse", - "nation", - "nature", - "naval", - "navet", - "naviguer", - "navrer", - "neige", - "nerf", - "nerveux", - "neuf", - "neutre", - "neuve", - "neveu", - "niche", - "nier", - "niveau", - "noble", - "noce", - "nocif", - "noir", - "nomade", - "nombre", - "nommer", - "nord", - "norme", - "notaire", - "notice", - "notre", - "nouer", - "nougat", - "nourrir", - "nous", - "nouveau", - "novice", - "noyade", - "noyer", - "nuage", - "nuance", - "nuire", - "nuit", - "nulle", - "nuque", - "oasis", - "objet", - "obliger", - "obscur", - "observer", - "obtenir", - "obus", - "occasion", - "occuper", - "ocre", - "octet", - "odeur", - "odorat", - "offense", - "officier", - "offrir", - "ogive", - "oiseau", - "olive", - "ombre", - "onctueux", - "onduler", - "ongle", - "onze", - "opter", - "option", - "orageux", - "oral", - "orange", - "orbite", - "ordinaire", - "ordre", - "oreille", - "organe", - "orgie", - "orgueil", - "orient", - "origan", - "orner", - "orteil", - "ortie", - "oser", - "osselet", - "otage", - "otarie", - "ouate", - "oublier", - "ouest", - "ours", - "outil", - "outre", - "ouvert", - "ouvrir", - "ovale", - "ozone", - "pacte", - "page", - "paille", - "pain", - "paire", - "paix", - "palace", - "palissade", - "palmier", - "palpiter", - "panda", - "panneau", - "papa", - "papier", - "paquet", - "parc", - "pardi", - "parfois", - "parler", - "parmi", - "parole", - "partir", - "parvenir", - "passer", - "pastel", - "patin", - "patron", - "paume", - "pause", - "pauvre", - "paver", - "pavot", - "payer", - "pays", - "peau", - "peigne", - "peinture", - "pelage", - "pelote", - "pencher", - "pendre", - "penser", - "pente", - "percer", - "perdu", - "perle", - "permettre", - "personne", - "perte", - "peser", - "pesticide", - "petit", - "peuple", - "peur", - "phase", - "photo", - "phrase", - "piano", - "pied", - "pierre", - "pieu", - "pile", - "pilier", - "pilote", - "pilule", - "piment", - "pincer", - "pinson", - "pinte", - "pion", - "piquer", - "pirate", - "pire", - "piste", - "piton", - "pitre", - "pivot", - "pizza", - "placer", - "plage", - "plaire", - "plan", - "plaque", - "plat", - "plein", - "pleurer", - "pliage", - "plier", - "plonger", - "plot", - "pluie", - "plume", - "plus", - "pneu", - "poche", - "podium", - "poids", - "poil", - "point", - "poire", - "poison", - "poitrine", - "poivre", - "police", - "pollen", - "pomme", - "pompier", - "poncer", - "pondre", - "pont", - "portion", - "poser", - "position", - "possible", - "poste", - "potage", - "potin", - "pouce", - "poudre", - "poulet", - "poumon", - "poupe", - "pour", - "pousser", - "poutre", - "pouvoir", - "prairie", - "premier", - "prendre", - "presque", - "preuve", - "prier", - "primeur", - "prince", - "prison", - "priver", - "prix", - "prochain", - "produire", - "profond", - "proie", - "projet", - "promener", - "prononcer", - "propre", - "prose", - "prouver", - "prune", - "public", - "puce", - "pudeur", - "puiser", - "pull", - "pulpe", - "puma", - "punir", - "purge", - "putois", - "quand", - "quartier", - "quasi", - "quatre", - "quel", - "question", - "queue", - "quiche", - "quille", - "quinze", - "quitter", - "quoi", - "rabais", - "raboter", - "race", - "racheter", - "racine", - "racler", - "raconter", - "radar", - "radio", - "rafale", - "rage", - "ragot", - "raideur", - "raie", - "rail", - "raison", - "ramasser", - "ramener", - "rampe", - "rance", - "rang", - "rapace", - "rapide", - "rapport", - "rarement", - "rasage", - "raser", - "rasoir", - "rassurer", - "rater", - "ratio", - "rature", - "ravage", - "ravir", - "rayer", - "rayon", - "rebond", - "recevoir", - "recherche", - "record", - "reculer", - "redevenir", - "refuser", - "regard", - "regretter", - "rein", - "rejeter", - "rejoindre", - "relation", - "relever", - "religion", - "remarquer", - "remettre", - "remise", - "remonter", - "remplir", - "remuer", - "rencontre", - "rendre", - "renier", - "renoncer", - "rentrer", - "renverser", - "repas", - "repli", - "reposer", - "reproche", - "requin", - "respect", - "ressembler", - "reste", - "retard", - "retenir", - "retirer", - "retour", - "retrouver", - "revenir", - "revoir", - "revue", - "rhume", - "ricaner", - "riche", - "rideau", - "ridicule", - "rien", - "rigide", - "rincer", - "rire", - "risquer", - "rituel", - "rivage", - "rive", - "robe", - "robot", - "robuste", - "rocade", - "roche", - "rodeur", - "rogner", - "roman", - "rompre", - "ronce", - "rondeur", - "ronger", - "roque", - "rose", - "rosir", - "rotation", - "rotule", - "roue", - "rouge", - "rouler", - "route", - "ruban", - "rubis", - "ruche", - "rude", - "ruelle", - "ruer", - "rugby", - "rugir", - "ruine", - "rumeur", - "rural", - "ruse", - "rustre", - "sable", - "sabot", - "sabre", - "sacre", - "sage", - "saint", - "saisir", - "salade", - "salive", - "salle", - "salon", - "salto", - "salut", - "salve", - "samba", - "sandale", - "sanguin", - "sapin", - "sarcasme", - "satisfaire", - "sauce", - "sauf", - "sauge", - "saule", - "sauna", - "sauter", - "sauver", - "savoir", - "science", - "scoop", - "score", - "second", - "secret", - "secte", - "seigneur", - "sein", - "seize", - "selle", - "selon", - "semaine", - "sembler", - "semer", - "semis", - "sensuel", - "sentir", - "sept", - "serpe", - "serrer", - "sertir", - "service", - "seuil", - "seulement", - "short", - "sien", - "sigle", - "signal", - "silence", - "silo", - "simple", - "singe", - "sinon", - "sinus", - "sioux", - "sirop", - "site", - "situation", - "skier", - "snob", - "sobre", - "social", - "socle", - "sodium", - "soigner", - "soir", - "soixante", - "soja", - "solaire", - "soldat", - "soleil", - "solide", - "solo", - "solvant", - "sombre", - "somme", - "somnoler", - "sondage", - "songeur", - "sonner", - "sorte", - "sosie", - "sottise", - "souci", - "soudain", - "souffrir", - "souhaiter", - "soulever", - "soumettre", - "soupe", - "sourd", - "soustraire", - "soutenir", - "souvent", - "soyeux", - "spectacle", - "sport", - "stade", - "stagiaire", - "stand", - "star", - "statue", - "stock", - "stop", - "store", - "style", - "suave", - "subir", - "sucre", - "suer", - "suffire", - "suie", - "suite", - "suivre", - "sujet", - "sulfite", - "supposer", - "surf", - "surprendre", - "surtout", - "surveiller", - "tabac", - "table", - "tabou", - "tache", - "tacler", - "tacot", - "tact", - "taie", - "taille", - "taire", - "talon", - "talus", - "tandis", - "tango", - "tanin", - "tant", - "taper", - "tapis", - "tard", - "tarif", - "tarot", - "tarte", - "tasse", - "taureau", - "taux", - "taverne", - "taxer", - "taxi", - "tellement", - "temple", - "tendre", - "tenir", - "tenter", - "tenu", - "terme", - "ternir", - "terre", - "test", - "texte", - "thym", - "tibia", - "tiers", - "tige", - "tipi", - "tique", - "tirer", - "tissu", - "titre", - "toast", - "toge", - "toile", - "toiser", - "toiture", - "tomber", - "tome", - "tonne", - "tonte", - "toque", - "torse", - "tortue", - "totem", - "toucher", - "toujours", - "tour", - "tousser", - "tout", - "toux", - "trace", - "train", - "trame", - "tranquille", - "travail", - "trembler", - "trente", - "tribu", - "trier", - "trio", - "tripe", - "triste", - "troc", - "trois", - "tromper", - "tronc", - "trop", - "trotter", - "trouer", - "truc", - "truite", - "tuba", - "tuer", - "tuile", - "turbo", - "tutu", - "tuyau", - "type", - "union", - "unique", - "unir", - "unisson", - "untel", - "urne", - "usage", - "user", - "usiner", - "usure", - "utile", - "vache", - "vague", - "vaincre", - "valeur", - "valoir", - "valser", - "valve", - "vampire", - "vaseux", - "vaste", - "veau", - "veille", - "veine", - "velours", - "velu", - "vendre", - "venir", - "vent", - "venue", - "verbe", - "verdict", - "version", - "vertige", - "verve", - "veste", - "veto", - "vexer", - "vice", - "victime", - "vide", - "vieil", - "vieux", - "vigie", - "vigne", - "ville", - "vingt", - "violent", - "virer", - "virus", - "visage", - "viser", - "visite", - "visuel", - "vitamine", - "vitrine", - "vivant", - "vivre", - "vocal", - "vodka", - "vogue", - "voici", - "voile", - "voir", - "voisin", - "voiture", - "volaille", - "volcan", - "voler", - "volt", - "votant", - "votre", - "vouer", - "vouloir", - "vous", - "voyage", - "voyou", - "vrac", - "vrai", - "yacht", - "yeti", - "yeux", - "yoga", - "zeste", - "zinc", - "zone", - "zoom" - ]; -} \ No newline at end of file + static const words = [ + "abandon", + "abattre", + "aboi", + "abolir", + "aborder", + "abri", + "absence", + "absolu", + "abuser", + "acacia", + "acajou", + "accent", + "accord", + "accrocher", + "accuser", + "acerbe", + "achat", + "acheter", + "acide", + "acier", + "acquis", + "acte", + "action", + "adage", + "adepte", + "adieu", + "admettre", + "admis", + "adorer", + "adresser", + "aduler", + "affaire", + "affirmer", + "afin", + "agacer", + "agent", + "agir", + "agiter", + "agonie", + "agrafe", + "agrume", + "aider", + "aigle", + "aigre", + "aile", + "ailleurs", + "aimant", + "aimer", + "ainsi", + "aise", + "ajouter", + "alarme", + "album", + "alcool", + "alerte", + "algue", + "alibi", + "aller", + "allumer", + "alors", + "amande", + "amener", + "amie", + "amorcer", + "amour", + "ample", + "amuser", + "ananas", + "ancien", + "anglais", + "angoisse", + "animal", + "anneau", + "annoncer", + "apercevoir", + "apparence", + "appel", + "apporter", + "apprendre", + "appuyer", + "arbre", + "arcade", + "arceau", + "arche", + "ardeur", + "argent", + "argile", + "aride", + "arme", + "armure", + "arracher", + "arriver", + "article", + "asile", + "aspect", + "assaut", + "assez", + "assister", + "assurer", + "astre", + "astuce", + "atlas", + "atroce", + "attacher", + "attente", + "attirer", + "aube", + "aucun", + "audace", + "auparavant", + "auquel", + "aurore", + "aussi", + "autant", + "auteur", + "autoroute", + "autre", + "aval", + "avant", + "avec", + "avenir", + "averse", + "aveu", + "avide", + "avion", + "avis", + "avoir", + "avouer", + "avril", + "azote", + "azur", + "badge", + "bagage", + "bague", + "bain", + "baisser", + "balai", + "balcon", + "balise", + "balle", + "bambou", + "banane", + "banc", + "bandage", + "banjo", + "banlieue", + "bannir", + "banque", + "baobab", + "barbe", + "barque", + "barrer", + "bassine", + "bataille", + "bateau", + "battre", + "baver", + "bavoir", + "bazar", + "beau", + "beige", + "berger", + "besoin", + "beurre", + "biais", + "biceps", + "bidule", + "bien", + "bijou", + "bilan", + "billet", + "blanc", + "blason", + "bleu", + "bloc", + "blond", + "bocal", + "boire", + "boiserie", + "boiter", + "bonbon", + "bondir", + "bonheur", + "bordure", + "borgne", + "borner", + "bosse", + "bouche", + "bouder", + "bouger", + "boule", + "bourse", + "bout", + "boxe", + "brader", + "braise", + "branche", + "braquer", + "bras", + "brave", + "brebis", + "brevet", + "brider", + "briller", + "brin", + "brique", + "briser", + "broche", + "broder", + "bronze", + "brosser", + "brouter", + "bruit", + "brute", + "budget", + "buffet", + "bulle", + "bureau", + "buriner", + "buste", + "buter", + "butiner", + "cabas", + "cabinet", + "cabri", + "cacao", + "cacher", + "cadeau", + "cadre", + "cage", + "caisse", + "caler", + "calme", + "camarade", + "camion", + "campagne", + "canal", + "canif", + "capable", + "capot", + "carat", + "caresser", + "carie", + "carpe", + "cartel", + "casier", + "casque", + "casserole", + "cause", + "cavale", + "cave", + "ceci", + "cela", + "celui", + "cendre", + "cent", + "cependant", + "cercle", + "cerise", + "cerner", + "certes", + "cerveau", + "cesser", + "chacun", + "chair", + "chaleur", + "chamois", + "chanson", + "chaque", + "charge", + "chasse", + "chat", + "chaud", + "chef", + "chemin", + "cheveu", + "chez", + "chicane", + "chien", + "chiffre", + "chiner", + "chiot", + "chlore", + "choc", + "choix", + "chose", + "chou", + "chute", + "cibler", + "cidre", + "ciel", + "cigale", + "cinq", + "cintre", + "cirage", + "cirque", + "ciseau", + "citation", + "citer", + "citron", + "civet", + "clairon", + "clan", + "classe", + "clavier", + "clef", + "climat", + "cloche", + "cloner", + "clore", + "clos", + "clou", + "club", + "cobra", + "cocon", + "coiffer", + "coin", + "colline", + "colon", + "combat", + "comme", + "compte", + "conclure", + "conduire", + "confier", + "connu", + "conseil", + "contre", + "convenir", + "copier", + "cordial", + "cornet", + "corps", + "cosmos", + "coton", + "couche", + "coude", + "couler", + "coupure", + "cour", + "couteau", + "couvrir", + "crabe", + "crainte", + "crampe", + "cran", + "creuser", + "crever", + "crier", + "crime", + "crin", + "crise", + "crochet", + "croix", + "cruel", + "cuisine", + "cuite", + "culot", + "culte", + "cumul", + "cure", + "curieux", + "cuve", + "dame", + "danger", + "dans", + "davantage", + "debout", + "dedans", + "dehors", + "delta", + "demain", + "demeurer", + "demi", + "dense", + "dent", + "depuis", + "dernier", + "descendre", + "dessus", + "destin", + "dette", + "deuil", + "deux", + "devant", + "devenir", + "devin", + "devoir", + "dicton", + "dieu", + "difficile", + "digestion", + "digue", + "diluer", + "dimanche", + "dinde", + "diode", + "dire", + "diriger", + "discours", + "disposer", + "distance", + "divan", + "divers", + "docile", + "docteur", + "dodu", + "dogme", + "doigt", + "dominer", + "donation", + "donjon", + "donner", + "dopage", + "dorer", + "dormir", + "doseur", + "douane", + "double", + "douche", + "douleur", + "doute", + "doux", + "douzaine", + "draguer", + "drame", + "drap", + "dresser", + "droit", + "duel", + "dune", + "duper", + "durant", + "durcir", + "durer", + "eaux", + "effacer", + "effet", + "effort", + "effrayant", + "elle", + "embrasser", + "emmener", + "emparer", + "empire", + "employer", + "emporter", + "enclos", + "encore", + "endive", + "endormir", + "endroit", + "enduit", + "enfant", + "enfermer", + "enfin", + "enfler", + "enfoncer", + "enfuir", + "engager", + "engin", + "enjeu", + "enlever", + "ennemi", + "ennui", + "ensemble", + "ensuite", + "entamer", + "entendre", + "entier", + "entourer", + "entre", + "envelopper", + "envie", + "envoyer", + "erreur", + "escalier", + "espace", + "espoir", + "esprit", + "essai", + "essor", + "essuyer", + "estimer", + "exact", + "examiner", + "excuse", + "exemple", + "exiger", + "exil", + "exister", + "exode", + "expliquer", + "exposer", + "exprimer", + "extase", + "fable", + "facette", + "facile", + "fade", + "faible", + "faim", + "faire", + "fait", + "falloir", + "famille", + "faner", + "farce", + "farine", + "fatigue", + "faucon", + "faune", + "faute", + "faux", + "faveur", + "favori", + "faxer", + "feinter", + "femme", + "fendre", + "fente", + "ferme", + "festin", + "feuille", + "feutre", + "fiable", + "fibre", + "ficher", + "fier", + "figer", + "figure", + "filet", + "fille", + "filmer", + "fils", + "filtre", + "final", + "finesse", + "finir", + "fiole", + "firme", + "fixe", + "flacon", + "flair", + "flamme", + "flan", + "flaque", + "fleur", + "flocon", + "flore", + "flot", + "flou", + "fluide", + "fluor", + "flux", + "focus", + "foin", + "foire", + "foison", + "folie", + "fonction", + "fondre", + "fonte", + "force", + "forer", + "forger", + "forme", + "fort", + "fosse", + "fouet", + "fouine", + "foule", + "four", + "foyer", + "frais", + "franc", + "frapper", + "freiner", + "frimer", + "friser", + "frite", + "froid", + "froncer", + "fruit", + "fugue", + "fuir", + "fuite", + "fumer", + "fureur", + "furieux", + "fuser", + "fusil", + "futile", + "futur", + "gagner", + "gain", + "gala", + "galet", + "galop", + "gamme", + "gant", + "garage", + "garde", + "garer", + "gauche", + "gaufre", + "gaule", + "gaver", + "gazon", + "geler", + "genou", + "genre", + "gens", + "gercer", + "germer", + "geste", + "gibier", + "gicler", + "gilet", + "girafe", + "givre", + "glace", + "glisser", + "globe", + "gloire", + "gluant", + "gober", + "golf", + "gommer", + "gorge", + "gosier", + "goutte", + "grain", + "gramme", + "grand", + "gras", + "grave", + "gredin", + "griffure", + "griller", + "gris", + "gronder", + "gros", + "grotte", + "groupe", + "grue", + "guerrier", + "guetter", + "guider", + "guise", + "habiter", + "hache", + "haie", + "haine", + "halte", + "hamac", + "hanche", + "hangar", + "hanter", + "haras", + "hareng", + "harpe", + "hasard", + "hausse", + "haut", + "havre", + "herbe", + "heure", + "hibou", + "hier", + "histoire", + "hiver", + "hochet", + "homme", + "honneur", + "honte", + "horde", + "horizon", + "hormone", + "houle", + "housse", + "hublot", + "huile", + "huit", + "humain", + "humble", + "humide", + "humour", + "hurler", + "idole", + "igloo", + "ignorer", + "illusion", + "image", + "immense", + "immobile", + "imposer", + "impression", + "incapable", + "inconnu", + "index", + "indiquer", + "infime", + "injure", + "inox", + "inspirer", + "instant", + "intention", + "intime", + "inutile", + "inventer", + "inviter", + "iode", + "iris", + "issue", + "ivre", + "jade", + "jadis", + "jamais", + "jambe", + "janvier", + "jardin", + "jauge", + "jaunisse", + "jeter", + "jeton", + "jeudi", + "jeune", + "joie", + "joindre", + "joli", + "joueur", + "journal", + "judo", + "juge", + "juillet", + "juin", + "jument", + "jungle", + "jupe", + "jupon", + "jurer", + "juron", + "jury", + "jusque", + "juste", + "kayak", + "ketchup", + "kilo", + "kiwi", + "koala", + "label", + "lacet", + "lacune", + "laine", + "laisse", + "lait", + "lame", + "lancer", + "lande", + "laque", + "lard", + "largeur", + "larme", + "larve", + "lasso", + "laver", + "lendemain", + "lentement", + "lequel", + "lettre", + "leur", + "lever", + "levure", + "liane", + "libre", + "lien", + "lier", + "lieutenant", + "ligne", + "ligoter", + "liguer", + "limace", + "limer", + "limite", + "lingot", + "lion", + "lire", + "lisser", + "litre", + "livre", + "lobe", + "local", + "logis", + "loin", + "loisir", + "long", + "loque", + "lors", + "lotus", + "louer", + "loup", + "lourd", + "louve", + "loyer", + "lubie", + "lucide", + "lueur", + "luge", + "luire", + "lundi", + "lune", + "lustre", + "lutin", + "lutte", + "luxe", + "machine", + "madame", + "magie", + "magnifique", + "magot", + "maigre", + "main", + "mairie", + "maison", + "malade", + "malheur", + "malin", + "manche", + "manger", + "manier", + "manoir", + "manquer", + "marche", + "mardi", + "marge", + "mariage", + "marquer", + "mars", + "masque", + "masse", + "matin", + "mauvais", + "meilleur", + "melon", + "membre", + "menacer", + "mener", + "mensonge", + "mentir", + "menu", + "merci", + "merlu", + "mesure", + "mettre", + "meuble", + "meunier", + "meute", + "miche", + "micro", + "midi", + "miel", + "miette", + "mieux", + "milieu", + "mille", + "mimer", + "mince", + "mineur", + "ministre", + "minute", + "mirage", + "miroir", + "miser", + "mite", + "mixte", + "mobile", + "mode", + "module", + "moins", + "mois", + "moment", + "momie", + "monde", + "monsieur", + "monter", + "moquer", + "moral", + "morceau", + "mordre", + "morose", + "morse", + "mortier", + "morue", + "motif", + "motte", + "moudre", + "moule", + "mourir", + "mousse", + "mouton", + "mouvement", + "moyen", + "muer", + "muette", + "mugir", + "muguet", + "mulot", + "multiple", + "munir", + "muret", + "muse", + "musique", + "muter", + "nacre", + "nager", + "nain", + "naissance", + "narine", + "narrer", + "naseau", + "nasse", + "nation", + "nature", + "naval", + "navet", + "naviguer", + "navrer", + "neige", + "nerf", + "nerveux", + "neuf", + "neutre", + "neuve", + "neveu", + "niche", + "nier", + "niveau", + "noble", + "noce", + "nocif", + "noir", + "nomade", + "nombre", + "nommer", + "nord", + "norme", + "notaire", + "notice", + "notre", + "nouer", + "nougat", + "nourrir", + "nous", + "nouveau", + "novice", + "noyade", + "noyer", + "nuage", + "nuance", + "nuire", + "nuit", + "nulle", + "nuque", + "oasis", + "objet", + "obliger", + "obscur", + "observer", + "obtenir", + "obus", + "occasion", + "occuper", + "ocre", + "octet", + "odeur", + "odorat", + "offense", + "officier", + "offrir", + "ogive", + "oiseau", + "olive", + "ombre", + "onctueux", + "onduler", + "ongle", + "onze", + "opter", + "option", + "orageux", + "oral", + "orange", + "orbite", + "ordinaire", + "ordre", + "oreille", + "organe", + "orgie", + "orgueil", + "orient", + "origan", + "orner", + "orteil", + "ortie", + "oser", + "osselet", + "otage", + "otarie", + "ouate", + "oublier", + "ouest", + "ours", + "outil", + "outre", + "ouvert", + "ouvrir", + "ovale", + "ozone", + "pacte", + "page", + "paille", + "pain", + "paire", + "paix", + "palace", + "palissade", + "palmier", + "palpiter", + "panda", + "panneau", + "papa", + "papier", + "paquet", + "parc", + "pardi", + "parfois", + "parler", + "parmi", + "parole", + "partir", + "parvenir", + "passer", + "pastel", + "patin", + "patron", + "paume", + "pause", + "pauvre", + "paver", + "pavot", + "payer", + "pays", + "peau", + "peigne", + "peinture", + "pelage", + "pelote", + "pencher", + "pendre", + "penser", + "pente", + "percer", + "perdu", + "perle", + "permettre", + "personne", + "perte", + "peser", + "pesticide", + "petit", + "peuple", + "peur", + "phase", + "photo", + "phrase", + "piano", + "pied", + "pierre", + "pieu", + "pile", + "pilier", + "pilote", + "pilule", + "piment", + "pincer", + "pinson", + "pinte", + "pion", + "piquer", + "pirate", + "pire", + "piste", + "piton", + "pitre", + "pivot", + "pizza", + "placer", + "plage", + "plaire", + "plan", + "plaque", + "plat", + "plein", + "pleurer", + "pliage", + "plier", + "plonger", + "plot", + "pluie", + "plume", + "plus", + "pneu", + "poche", + "podium", + "poids", + "poil", + "point", + "poire", + "poison", + "poitrine", + "poivre", + "police", + "pollen", + "pomme", + "pompier", + "poncer", + "pondre", + "pont", + "portion", + "poser", + "position", + "possible", + "poste", + "potage", + "potin", + "pouce", + "poudre", + "poulet", + "poumon", + "poupe", + "pour", + "pousser", + "poutre", + "pouvoir", + "prairie", + "premier", + "prendre", + "presque", + "preuve", + "prier", + "primeur", + "prince", + "prison", + "priver", + "prix", + "prochain", + "produire", + "profond", + "proie", + "projet", + "promener", + "prononcer", + "propre", + "prose", + "prouver", + "prune", + "public", + "puce", + "pudeur", + "puiser", + "pull", + "pulpe", + "puma", + "punir", + "purge", + "putois", + "quand", + "quartier", + "quasi", + "quatre", + "quel", + "question", + "queue", + "quiche", + "quille", + "quinze", + "quitter", + "quoi", + "rabais", + "raboter", + "race", + "racheter", + "racine", + "racler", + "raconter", + "radar", + "radio", + "rafale", + "rage", + "ragot", + "raideur", + "raie", + "rail", + "raison", + "ramasser", + "ramener", + "rampe", + "rance", + "rang", + "rapace", + "rapide", + "rapport", + "rarement", + "rasage", + "raser", + "rasoir", + "rassurer", + "rater", + "ratio", + "rature", + "ravage", + "ravir", + "rayer", + "rayon", + "rebond", + "recevoir", + "recherche", + "record", + "reculer", + "redevenir", + "refuser", + "regard", + "regretter", + "rein", + "rejeter", + "rejoindre", + "relation", + "relever", + "religion", + "remarquer", + "remettre", + "remise", + "remonter", + "remplir", + "remuer", + "rencontre", + "rendre", + "renier", + "renoncer", + "rentrer", + "renverser", + "repas", + "repli", + "reposer", + "reproche", + "requin", + "respect", + "ressembler", + "reste", + "retard", + "retenir", + "retirer", + "retour", + "retrouver", + "revenir", + "revoir", + "revue", + "rhume", + "ricaner", + "riche", + "rideau", + "ridicule", + "rien", + "rigide", + "rincer", + "rire", + "risquer", + "rituel", + "rivage", + "rive", + "robe", + "robot", + "robuste", + "rocade", + "roche", + "rodeur", + "rogner", + "roman", + "rompre", + "ronce", + "rondeur", + "ronger", + "roque", + "rose", + "rosir", + "rotation", + "rotule", + "roue", + "rouge", + "rouler", + "route", + "ruban", + "rubis", + "ruche", + "rude", + "ruelle", + "ruer", + "rugby", + "rugir", + "ruine", + "rumeur", + "rural", + "ruse", + "rustre", + "sable", + "sabot", + "sabre", + "sacre", + "sage", + "saint", + "saisir", + "salade", + "salive", + "salle", + "salon", + "salto", + "salut", + "salve", + "samba", + "sandale", + "sanguin", + "sapin", + "sarcasme", + "satisfaire", + "sauce", + "sauf", + "sauge", + "saule", + "sauna", + "sauter", + "sauver", + "savoir", + "science", + "scoop", + "score", + "second", + "secret", + "secte", + "seigneur", + "sein", + "seize", + "selle", + "selon", + "semaine", + "sembler", + "semer", + "semis", + "sensuel", + "sentir", + "sept", + "serpe", + "serrer", + "sertir", + "service", + "seuil", + "seulement", + "short", + "sien", + "sigle", + "signal", + "silence", + "silo", + "simple", + "singe", + "sinon", + "sinus", + "sioux", + "sirop", + "site", + "situation", + "skier", + "snob", + "sobre", + "social", + "socle", + "sodium", + "soigner", + "soir", + "soixante", + "soja", + "solaire", + "soldat", + "soleil", + "solide", + "solo", + "solvant", + "sombre", + "somme", + "somnoler", + "sondage", + "songeur", + "sonner", + "sorte", + "sosie", + "sottise", + "souci", + "soudain", + "souffrir", + "souhaiter", + "soulever", + "soumettre", + "soupe", + "sourd", + "soustraire", + "soutenir", + "souvent", + "soyeux", + "spectacle", + "sport", + "stade", + "stagiaire", + "stand", + "star", + "statue", + "stock", + "stop", + "store", + "style", + "suave", + "subir", + "sucre", + "suer", + "suffire", + "suie", + "suite", + "suivre", + "sujet", + "sulfite", + "supposer", + "surf", + "surprendre", + "surtout", + "surveiller", + "tabac", + "table", + "tabou", + "tache", + "tacler", + "tacot", + "tact", + "taie", + "taille", + "taire", + "talon", + "talus", + "tandis", + "tango", + "tanin", + "tant", + "taper", + "tapis", + "tard", + "tarif", + "tarot", + "tarte", + "tasse", + "taureau", + "taux", + "taverne", + "taxer", + "taxi", + "tellement", + "temple", + "tendre", + "tenir", + "tenter", + "tenu", + "terme", + "ternir", + "terre", + "test", + "texte", + "thym", + "tibia", + "tiers", + "tige", + "tipi", + "tique", + "tirer", + "tissu", + "titre", + "toast", + "toge", + "toile", + "toiser", + "toiture", + "tomber", + "tome", + "tonne", + "tonte", + "toque", + "torse", + "tortue", + "totem", + "toucher", + "toujours", + "tour", + "tousser", + "tout", + "toux", + "trace", + "train", + "trame", + "tranquille", + "travail", + "trembler", + "trente", + "tribu", + "trier", + "trio", + "tripe", + "triste", + "troc", + "trois", + "tromper", + "tronc", + "trop", + "trotter", + "trouer", + "truc", + "truite", + "tuba", + "tuer", + "tuile", + "turbo", + "tutu", + "tuyau", + "type", + "union", + "unique", + "unir", + "unisson", + "untel", + "urne", + "usage", + "user", + "usiner", + "usure", + "utile", + "vache", + "vague", + "vaincre", + "valeur", + "valoir", + "valser", + "valve", + "vampire", + "vaseux", + "vaste", + "veau", + "veille", + "veine", + "velours", + "velu", + "vendre", + "venir", + "vent", + "venue", + "verbe", + "verdict", + "version", + "vertige", + "verve", + "veste", + "veto", + "vexer", + "vice", + "victime", + "vide", + "vieil", + "vieux", + "vigie", + "vigne", + "ville", + "vingt", + "violent", + "virer", + "virus", + "visage", + "viser", + "visite", + "visuel", + "vitamine", + "vitrine", + "vivant", + "vivre", + "vocal", + "vodka", + "vogue", + "voici", + "voile", + "voir", + "voisin", + "voiture", + "volaille", + "volcan", + "voler", + "volt", + "votant", + "votre", + "vouer", + "vouloir", + "vous", + "voyage", + "voyou", + "vrac", + "vrai", + "yacht", + "yeti", + "yeux", + "yoga", + "zeste", + "zinc", + "zone", + "zoom" + ]; +} diff --git a/cw_wownero/lib/mnemonics/german.dart b/cw_wownero/lib/mnemonics/german.dart index 1491c9b0e..8e775f428 100644 --- a/cw_wownero/lib/mnemonics/german.dart +++ b/cw_wownero/lib/mnemonics/german.dart @@ -1627,4 +1627,4 @@ class GermanMnemonics { "Zweck", "Zyklop" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/italian.dart b/cw_wownero/lib/mnemonics/italian.dart index 275f85bf4..2cad99876 100644 --- a/cw_wownero/lib/mnemonics/italian.dart +++ b/cw_wownero/lib/mnemonics/italian.dart @@ -1,1630 +1,1630 @@ class ItalianMnemonics { - static const words = [ - "abbinare", - "abbonato", - "abisso", - "abitare", - "abominio", - "accadere", - "accesso", - "acciaio", - "accordo", - "accumulo", - "acido", - "acqua", - "acrobata", - "acustico", - "adattare", - "addetto", - "addio", - "addome", - "adeguato", - "aderire", - "adorare", - "adottare", - "adozione", - "adulto", - "aereo", - "aerobica", - "affare", - "affetto", - "affidare", - "affogato", - "affronto", - "africano", - "afrodite", - "agenzia", - "aggancio", - "aggeggio", - "aggiunta", - "agio", - "agire", - "agitare", - "aglio", - "agnello", - "agosto", - "aiutare", - "albero", - "albo", - "alce", - "alchimia", - "alcool", - "alfabeto", - "algebra", - "alimento", - "allarme", - "alleanza", - "allievo", - "alloggio", - "alluce", - "alpi", - "alterare", - "altro", - "aluminio", - "amante", - "amarezza", - "ambiente", - "ambrosia", - "america", - "amico", - "ammalare", - "ammirare", - "amnesia", - "amnistia", - "amore", - "ampliare", - "amputare", - "analisi", - "anamnesi", - "ananas", - "anarchia", - "anatra", - "anca", - "ancorato", - "andare", - "androide", - "aneddoto", - "anello", - "angelo", - "angolino", - "anguilla", - "anidride", - "anima", - "annegare", - "anno", - "annuncio", - "anomalia", - "antenna", - "anticipo", - "aperto", - "apostolo", - "appalto", - "appello", - "appiglio", - "applauso", - "appoggio", - "appurare", - "aprile", - "aquila", - "arabo", - "arachidi", - "aragosta", - "arancia", - "arbitrio", - "archivio", - "arco", - "argento", - "argilla", - "aria", - "ariete", - "arma", - "armonia", - "aroma", - "arrivare", - "arrosto", - "arsenale", - "arte", - "artiglio", - "asfalto", - "asfissia", - "asino", - "asparagi", - "aspirina", - "assalire", - "assegno", - "assolto", - "assurdo", - "asta", - "astratto", - "atlante", - "atletica", - "atomo", - "atropina", - "attacco", - "attesa", - "attico", - "atto", - "attrarre", - "auguri", - "aula", - "aumento", - "aurora", - "auspicio", - "autista", - "auto", - "autunno", - "avanzare", - "avarizia", - "avere", - "aviatore", - "avido", - "avorio", - "avvenire", - "avviso", - "avvocato", - "azienda", - "azione", - "azzardo", - "azzurro", - "babbuino", - "bacio", - "badante", - "baffi", - "bagaglio", - "bagliore", - "bagno", - "balcone", - "balena", - "ballare", - "balordo", - "balsamo", - "bambola", - "bancomat", - "banda", - "barato", - "barba", - "barista", - "barriera", - "basette", - "basilico", - "bassista", - "bastare", - "battello", - "bavaglio", - "beccare", - "beduino", - "bellezza", - "bene", - "benzina", - "berretto", - "bestia", - "bevitore", - "bianco", - "bibbia", - "biberon", - "bibita", - "bici", - "bidone", - "bilancia", - "biliardo", - "binario", - "binocolo", - "biologia", - "biondina", - "biopsia", - "biossido", - "birbante", - "birra", - "biscotto", - "bisogno", - "bistecca", - "bivio", - "blindare", - "bloccare", - "bocca", - "bollire", - "bombola", - "bonifico", - "borghese", - "borsa", - "bottino", - "botulino", - "braccio", - "bradipo", - "branco", - "bravo", - "bresaola", - "bretelle", - "brevetto", - "briciola", - "brigante", - "brillare", - "brindare", - "brivido", - "broccoli", - "brontolo", - "bruciare", - "brufolo", - "bucare", - "buddista", - "budino", - "bufera", - "buffo", - "bugiardo", - "buio", - "buono", - "burrone", - "bussola", - "bustina", - "buttare", - "cabernet", - "cabina", - "cacao", - "cacciare", - "cactus", - "cadavere", - "caffe", - "calamari", - "calcio", - "caldaia", - "calmare", - "calunnia", - "calvario", - "calzone", - "cambiare", - "camera", - "camion", - "cammello", - "campana", - "canarino", - "cancello", - "candore", - "cane", - "canguro", - "cannone", - "canoa", - "cantare", - "canzone", - "caos", - "capanna", - "capello", - "capire", - "capo", - "capperi", - "capra", - "capsula", - "caraffa", - "carbone", - "carciofo", - "cardigan", - "carenza", - "caricare", - "carota", - "carrello", - "carta", - "casa", - "cascare", - "caserma", - "cashmere", - "casino", - "cassetta", - "castello", - "catalogo", - "catena", - "catorcio", - "cattivo", - "causa", - "cauzione", - "cavallo", - "caverna", - "caviglia", - "cavo", - "cazzotto", - "celibato", - "cemento", - "cenare", - "centrale", - "ceramica", - "cercare", - "ceretta", - "cerniera", - "certezza", - "cervello", - "cessione", - "cestino", - "cetriolo", - "chiave", - "chiedere", - "chilo", - "chimera", - "chiodo", - "chirurgo", - "chitarra", - "chiudere", - "ciabatta", - "ciao", - "cibo", - "ciccia", - "cicerone", - "ciclone", - "cicogna", - "cielo", - "cifra", - "cigno", - "ciliegia", - "cimitero", - "cinema", - "cinque", - "cintura", - "ciondolo", - "ciotola", - "cipolla", - "cippato", - "circuito", - "cisterna", - "citofono", - "ciuccio", - "civetta", - "civico", - "clausola", - "cliente", - "clima", - "clinica", - "cobra", - "coccole", - "cocktail", - "cocomero", - "codice", - "coesione", - "cogliere", - "cognome", - "colla", - "colomba", - "colpire", - "coltello", - "comando", - "comitato", - "commedia", - "comodino", - "compagna", - "comune", - "concerto", - "condotto", - "conforto", - "congiura", - "coniglio", - "consegna", - "conto", - "convegno", - "coperta", - "copia", - "coprire", - "corazza", - "corda", - "corleone", - "cornice", - "corona", - "corpo", - "corrente", - "corsa", - "cortesia", - "corvo", - "coso", - "costume", - "cotone", - "cottura", - "cozza", - "crampo", - "cratere", - "cravatta", - "creare", - "credere", - "crema", - "crescere", - "crimine", - "criterio", - "croce", - "crollare", - "cronaca", - "crostata", - "croupier", - "cubetto", - "cucciolo", - "cucina", - "cultura", - "cuoco", - "cuore", - "cupido", - "cupola", - "cura", - "curva", - "cuscino", - "custode", - "danzare", - "data", - "decennio", - "decidere", - "decollo", - "dedicare", - "dedurre", - "definire", - "delegare", - "delfino", - "delitto", - "demone", - "dentista", - "denuncia", - "deposito", - "derivare", - "deserto", - "designer", - "destino", - "detonare", - "dettagli", - "diagnosi", - "dialogo", - "diamante", - "diario", - "diavolo", - "dicembre", - "difesa", - "digerire", - "digitare", - "diluvio", - "dinamica", - "dipinto", - "diploma", - "diramare", - "dire", - "dirigere", - "dirupo", - "discesa", - "disdetta", - "disegno", - "disporre", - "dissenso", - "distacco", - "dito", - "ditta", - "diva", - "divenire", - "dividere", - "divorare", - "docente", - "dolcetto", - "dolore", - "domatore", - "domenica", - "dominare", - "donatore", - "donna", - "dorato", - "dormire", - "dorso", - "dosaggio", - "dottore", - "dovere", - "download", - "dragone", - "dramma", - "dubbio", - "dubitare", - "duetto", - "durata", - "ebbrezza", - "eccesso", - "eccitare", - "eclissi", - "economia", - "edera", - "edificio", - "editore", - "edizione", - "educare", - "effetto", - "egitto", - "egiziano", - "elastico", - "elefante", - "eleggere", - "elemento", - "elenco", - "elezione", - "elmetto", - "elogio", - "embrione", - "emergere", - "emettere", - "eminenza", - "emisfero", - "emozione", - "empatia", - "energia", - "enfasi", - "enigma", - "entrare", - "enzima", - "epidemia", - "epilogo", - "episodio", - "epoca", - "equivoco", - "erba", - "erede", - "eroe", - "erotico", - "errore", - "eruzione", - "esaltare", - "esame", - "esaudire", - "eseguire", - "esempio", - "esigere", - "esistere", - "esito", - "esperto", - "espresso", - "essere", - "estasi", - "esterno", - "estrarre", - "eterno", - "etica", - "euforico", - "europa", - "evacuare", - "evasione", - "evento", - "evidenza", - "evitare", - "evolvere", - "fabbrica", - "facciata", - "fagiano", - "fagotto", - "falco", - "fame", - "famiglia", - "fanale", - "fango", - "fantasia", - "farfalla", - "farmacia", - "faro", - "fase", - "fastidio", - "faticare", - "fatto", - "favola", - "febbre", - "femmina", - "femore", - "fenomeno", - "fermata", - "feromoni", - "ferrari", - "fessura", - "festa", - "fiaba", - "fiamma", - "fianco", - "fiat", - "fibbia", - "fidare", - "fieno", - "figa", - "figlio", - "figura", - "filetto", - "filmato", - "filosofo", - "filtrare", - "finanza", - "finestra", - "fingere", - "finire", - "finta", - "finzione", - "fiocco", - "fioraio", - "firewall", - "firmare", - "fisico", - "fissare", - "fittizio", - "fiume", - "flacone", - "flagello", - "flirtare", - "flusso", - "focaccia", - "foglio", - "fognario", - "follia", - "fonderia", - "fontana", - "forbici", - "forcella", - "foresta", - "forgiare", - "formare", - "fornace", - "foro", - "fortuna", - "forzare", - "fosforo", - "fotoni", - "fracasso", - "fragola", - "frantumi", - "fratello", - "frazione", - "freccia", - "freddo", - "frenare", - "fresco", - "friggere", - "frittata", - "frivolo", - "frizione", - "fronte", - "frullato", - "frumento", - "frusta", - "frutto", - "fucile", - "fuggire", - "fulmine", - "fumare", - "funzione", - "fuoco", - "furbizia", - "furgone", - "furia", - "furore", - "fusibile", - "fuso", - "futuro", - "gabbiano", - "galassia", - "gallina", - "gamba", - "gancio", - "garanzia", - "garofano", - "gasolio", - "gatto", - "gazebo", - "gazzetta", - "gelato", - "gemelli", - "generare", - "genitori", - "gennaio", - "geologia", - "germania", - "gestire", - "gettare", - "ghepardo", - "ghiaccio", - "giaccone", - "giaguaro", - "giallo", - "giappone", - "giardino", - "gigante", - "gioco", - "gioiello", - "giorno", - "giovane", - "giraffa", - "giudizio", - "giurare", - "giusto", - "globo", - "gloria", - "glucosio", - "gnocca", - "gocciola", - "godere", - "gomito", - "gomma", - "gonfiare", - "gorilla", - "governo", - "gradire", - "graffiti", - "granchio", - "grappolo", - "grasso", - "grattare", - "gridare", - "grissino", - "grondaia", - "grugnito", - "gruppo", - "guadagno", - "guaio", - "guancia", - "guardare", - "gufo", - "guidare", - "guscio", - "gusto", - "icona", - "idea", - "identico", - "idolo", - "idoneo", - "idrante", - "idrogeno", - "igiene", - "ignoto", - "imbarco", - "immagine", - "immobile", - "imparare", - "impedire", - "impianto", - "importo", - "impresa", - "impulso", - "incanto", - "incendio", - "incidere", - "incontro", - "incrocia", - "incubo", - "indagare", - "indice", - "indotto", - "infanzia", - "inferno", - "infinito", - "infranto", - "ingerire", - "inglese", - "ingoiare", - "ingresso", - "iniziare", - "innesco", - "insalata", - "inserire", - "insicuro", - "insonnia", - "insulto", - "interno", - "introiti", - "invasori", - "inverno", - "invito", - "invocare", - "ipnosi", - "ipocrita", - "ipotesi", - "ironia", - "irrigare", - "iscritto", - "isola", - "ispirare", - "isterico", - "istinto", - "istruire", - "italiano", - "jazz", - "labbra", - "labrador", - "ladro", - "lago", - "lamento", - "lampone", - "lancetta", - "lanterna", - "lapide", - "larva", - "lasagne", - "lasciare", - "lastra", - "latte", - "laurea", - "lavagna", - "lavorare", - "leccare", - "legare", - "leggere", - "lenzuolo", - "leone", - "lepre", - "letargo", - "lettera", - "levare", - "levitare", - "lezione", - "liberare", - "libidine", - "libro", - "licenza", - "lievito", - "limite", - "lince", - "lingua", - "liquore", - "lire", - "listino", - "litigare", - "litro", - "locale", - "lottare", - "lucciola", - "lucidare", - "luglio", - "luna", - "macchina", - "madama", - "madre", - "maestro", - "maggio", - "magico", - "maglione", - "magnolia", - "mago", - "maialino", - "maionese", - "malattia", - "male", - "malloppo", - "mancare", - "mandorla", - "mangiare", - "manico", - "manopola", - "mansarda", - "mantello", - "manubrio", - "manzo", - "mappa", - "mare", - "margine", - "marinaio", - "marmotta", - "marocco", - "martello", - "marzo", - "maschera", - "matrice", - "maturare", - "mazzetta", - "meandri", - "medaglia", - "medico", - "medusa", - "megafono", - "melone", - "membrana", - "menta", - "mercato", - "meritare", - "merluzzo", - "mese", - "mestiere", - "metafora", - "meteo", - "metodo", - "mettere", - "miele", - "miglio", - "miliardo", - "mimetica", - "minatore", - "minuto", - "miracolo", - "mirtillo", - "missile", - "mistero", - "misura", - "mito", - "mobile", - "moda", - "moderare", - "moglie", - "molecola", - "molle", - "momento", - "moneta", - "mongolia", - "monologo", - "montagna", - "morale", - "morbillo", - "mordere", - "mosaico", - "mosca", - "mostro", - "motivare", - "moto", - "mulino", - "mulo", - "muovere", - "muraglia", - "muscolo", - "museo", - "musica", - "mutande", - "nascere", - "nastro", - "natale", - "natura", - "nave", - "navigare", - "negare", - "negozio", - "nemico", - "nero", - "nervo", - "nessuno", - "nettare", - "neutroni", - "neve", - "nevicare", - "nicotina", - "nido", - "nipote", - "nocciola", - "noleggio", - "nome", - "nonno", - "norvegia", - "notare", - "notizia", - "nove", - "nucleo", - "nuda", - "nuotare", - "nutrire", - "obbligo", - "occhio", - "occupare", - "oceano", - "odissea", - "odore", - "offerta", - "officina", - "offrire", - "oggetto", - "oggi", - "olfatto", - "olio", - "oliva", - "ombelico", - "ombrello", - "omuncolo", - "ondata", - "onore", - "opera", - "opinione", - "opuscolo", - "opzione", - "orario", - "orbita", - "orchidea", - "ordine", - "orecchio", - "orgasmo", - "orgoglio", - "origine", - "orologio", - "oroscopo", - "orso", - "oscurare", - "ospedale", - "ospite", - "ossigeno", - "ostacolo", - "ostriche", - "ottenere", - "ottimo", - "ottobre", - "ovest", - "pacco", - "pace", - "pacifico", - "padella", - "pagare", - "pagina", - "pagnotta", - "palazzo", - "palestra", - "palpebre", - "pancetta", - "panfilo", - "panino", - "pannello", - "panorama", - "papa", - "paperino", - "paradiso", - "parcella", - "parente", - "parlare", - "parodia", - "parrucca", - "partire", - "passare", - "pasta", - "patata", - "patente", - "patogeno", - "patriota", - "pausa", - "pazienza", - "peccare", - "pecora", - "pedalare", - "pelare", - "pena", - "pendenza", - "penisola", - "pennello", - "pensare", - "pentirsi", - "percorso", - "perdono", - "perfetto", - "perizoma", - "perla", - "permesso", - "persona", - "pesare", - "pesce", - "peso", - "petardo", - "petrolio", - "pezzo", - "piacere", - "pianeta", - "piastra", - "piatto", - "piazza", - "piccolo", - "piede", - "piegare", - "pietra", - "pigiama", - "pigliare", - "pigrizia", - "pilastro", - "pilota", - "pinguino", - "pioggia", - "piombo", - "pionieri", - "piovra", - "pipa", - "pirata", - "pirolisi", - "piscina", - "pisolino", - "pista", - "pitone", - "piumino", - "pizza", - "plastica", - "platino", - "poesia", - "poiana", - "polaroid", - "polenta", - "polimero", - "pollo", - "polmone", - "polpetta", - "poltrona", - "pomodoro", - "pompa", - "popolo", - "porco", - "porta", - "porzione", - "possesso", - "postino", - "potassio", - "potere", - "poverino", - "pranzo", - "prato", - "prefisso", - "prelievo", - "premio", - "prendere", - "prestare", - "pretesa", - "prezzo", - "primario", - "privacy", - "problema", - "processo", - "prodotto", - "profeta", - "progetto", - "promessa", - "pronto", - "proposta", - "proroga", - "prossimo", - "proteina", - "prova", - "prudenza", - "pubblico", - "pudore", - "pugilato", - "pulire", - "pulsante", - "puntare", - "pupazzo", - "puzzle", - "quaderno", - "qualcuno", - "quarzo", - "quercia", - "quintale", - "rabbia", - "racconto", - "radice", - "raffica", - "ragazza", - "ragione", - "rammento", - "ramo", - "rana", - "randagio", - "rapace", - "rapinare", - "rapporto", - "rasatura", - "ravioli", - "reagire", - "realista", - "reattore", - "reazione", - "recitare", - "recluso", - "record", - "recupero", - "redigere", - "regalare", - "regina", - "regola", - "relatore", - "reliquia", - "remare", - "rendere", - "reparto", - "resina", - "resto", - "rete", - "retorica", - "rettile", - "revocare", - "riaprire", - "ribadire", - "ribelle", - "ricambio", - "ricetta", - "richiamo", - "ricordo", - "ridurre", - "riempire", - "riferire", - "riflesso", - "righello", - "rilancio", - "rilevare", - "rilievo", - "rimanere", - "rimborso", - "rinforzo", - "rinuncia", - "riparo", - "ripetere", - "riposare", - "ripulire", - "risalita", - "riscatto", - "riserva", - "riso", - "rispetto", - "ritaglio", - "ritmo", - "ritorno", - "ritratto", - "rituale", - "riunione", - "riuscire", - "riva", - "robotica", - "rondine", - "rosa", - "rospo", - "rosso", - "rotonda", - "rotta", - "roulotte", - "rubare", - "rubrica", - "ruffiano", - "rumore", - "ruota", - "ruscello", - "sabbia", - "sacco", - "saggio", - "sale", - "salire", - "salmone", - "salto", - "salutare", - "salvia", - "sangue", - "sanzioni", - "sapere", - "sapienza", - "sarcasmo", - "sardine", - "sartoria", - "sbalzo", - "sbarcare", - "sberla", - "sborsare", - "scadenza", - "scafo", - "scala", - "scambio", - "scappare", - "scarpa", - "scatola", - "scelta", - "scena", - "sceriffo", - "scheggia", - "schiuma", - "sciarpa", - "scienza", - "scimmia", - "sciopero", - "scivolo", - "sclerare", - "scolpire", - "sconto", - "scopa", - "scordare", - "scossa", - "scrivere", - "scrupolo", - "scuderia", - "scultore", - "scuola", - "scusare", - "sdraiare", - "secolo", - "sedativo", - "sedere", - "sedia", - "segare", - "segreto", - "seguire", - "semaforo", - "seme", - "senape", - "seno", - "sentiero", - "separare", - "sepolcro", - "sequenza", - "serata", - "serpente", - "servizio", - "sesso", - "seta", - "settore", - "sfamare", - "sfera", - "sfidare", - "sfiorare", - "sfogare", - "sgabello", - "sicuro", - "siepe", - "sigaro", - "silenzio", - "silicone", - "simbiosi", - "simpatia", - "simulare", - "sinapsi", - "sindrome", - "sinergia", - "sinonimo", - "sintonia", - "sirena", - "siringa", - "sistema", - "sito", - "smalto", - "smentire", - "smontare", - "soccorso", - "socio", - "soffitto", - "software", - "soggetto", - "sogliola", - "sognare", - "soldi", - "sole", - "sollievo", - "solo", - "sommario", - "sondare", - "sonno", - "sorpresa", - "sorriso", - "sospiro", - "sostegno", - "sovrano", - "spaccare", - "spada", - "spagnolo", - "spalla", - "sparire", - "spavento", - "spazio", - "specchio", - "spedire", - "spegnere", - "spendere", - "speranza", - "spessore", - "spezzare", - "spiaggia", - "spiccare", - "spiegare", - "spiffero", - "spingere", - "sponda", - "sporcare", - "spostare", - "spremuta", - "spugna", - "spumante", - "spuntare", - "squadra", - "squillo", - "staccare", - "stadio", - "stagione", - "stallone", - "stampa", - "stancare", - "starnuto", - "statura", - "stella", - "stendere", - "sterzo", - "stilista", - "stimolo", - "stinco", - "stiva", - "stoffa", - "storia", - "strada", - "stregone", - "striscia", - "studiare", - "stufa", - "stupendo", - "subire", - "successo", - "sudare", - "suono", - "superare", - "supporto", - "surfista", - "sussurro", - "svelto", - "svenire", - "sviluppo", - "svolta", - "svuotare", - "tabacco", - "tabella", - "tabu", - "tacchino", - "tacere", - "taglio", - "talento", - "tangente", - "tappeto", - "tartufo", - "tassello", - "tastiera", - "tavolo", - "tazza", - "teatro", - "tedesco", - "telaio", - "telefono", - "tema", - "temere", - "tempo", - "tendenza", - "tenebre", - "tensione", - "tentare", - "teologia", - "teorema", - "termica", - "terrazzo", - "teschio", - "tesi", - "tesoro", - "tessera", - "testa", - "thriller", - "tifoso", - "tigre", - "timbrare", - "timido", - "tinta", - "tirare", - "tisana", - "titano", - "titolo", - "toccare", - "togliere", - "topolino", - "torcia", - "torrente", - "tovaglia", - "traffico", - "tragitto", - "training", - "tramonto", - "transito", - "trapezio", - "trasloco", - "trattore", - "trazione", - "treccia", - "tregua", - "treno", - "triciclo", - "tridente", - "trilogia", - "tromba", - "troncare", - "trota", - "trovare", - "trucco", - "tubo", - "tulipano", - "tumulto", - "tunisia", - "tuono", - "turista", - "tuta", - "tutelare", - "tutore", - "ubriaco", - "uccello", - "udienza", - "udito", - "uffa", - "umanoide", - "umore", - "unghia", - "unguento", - "unicorno", - "unione", - "universo", - "uomo", - "uragano", - "uranio", - "urlare", - "uscire", - "utente", - "utilizzo", - "vacanza", - "vacca", - "vaglio", - "vagonata", - "valle", - "valore", - "valutare", - "valvola", - "vampiro", - "vaniglia", - "vanto", - "vapore", - "variante", - "vasca", - "vaselina", - "vassoio", - "vedere", - "vegetale", - "veglia", - "veicolo", - "vela", - "veleno", - "velivolo", - "velluto", - "vendere", - "venerare", - "venire", - "vento", - "veranda", - "verbo", - "verdura", - "vergine", - "verifica", - "vernice", - "vero", - "verruca", - "versare", - "vertebra", - "vescica", - "vespaio", - "vestito", - "vesuvio", - "veterano", - "vetro", - "vetta", - "viadotto", - "viaggio", - "vibrare", - "vicenda", - "vichingo", - "vietare", - "vigilare", - "vigneto", - "villa", - "vincere", - "violino", - "vipera", - "virgola", - "virtuoso", - "visita", - "vita", - "vitello", - "vittima", - "vivavoce", - "vivere", - "viziato", - "voglia", - "volare", - "volpe", - "volto", - "volume", - "vongole", - "voragine", - "vortice", - "votare", - "vulcano", - "vuotare", - "zabaione", - "zaffiro", - "zainetto", - "zampa", - "zanzara", - "zattera", - "zavorra", - "zenzero", - "zero", - "zingaro", - "zittire", - "zoccolo", - "zolfo", - "zombie", - "zucchero" - ]; -} \ No newline at end of file + static const words = [ + "abbinare", + "abbonato", + "abisso", + "abitare", + "abominio", + "accadere", + "accesso", + "acciaio", + "accordo", + "accumulo", + "acido", + "acqua", + "acrobata", + "acustico", + "adattare", + "addetto", + "addio", + "addome", + "adeguato", + "aderire", + "adorare", + "adottare", + "adozione", + "adulto", + "aereo", + "aerobica", + "affare", + "affetto", + "affidare", + "affogato", + "affronto", + "africano", + "afrodite", + "agenzia", + "aggancio", + "aggeggio", + "aggiunta", + "agio", + "agire", + "agitare", + "aglio", + "agnello", + "agosto", + "aiutare", + "albero", + "albo", + "alce", + "alchimia", + "alcool", + "alfabeto", + "algebra", + "alimento", + "allarme", + "alleanza", + "allievo", + "alloggio", + "alluce", + "alpi", + "alterare", + "altro", + "aluminio", + "amante", + "amarezza", + "ambiente", + "ambrosia", + "america", + "amico", + "ammalare", + "ammirare", + "amnesia", + "amnistia", + "amore", + "ampliare", + "amputare", + "analisi", + "anamnesi", + "ananas", + "anarchia", + "anatra", + "anca", + "ancorato", + "andare", + "androide", + "aneddoto", + "anello", + "angelo", + "angolino", + "anguilla", + "anidride", + "anima", + "annegare", + "anno", + "annuncio", + "anomalia", + "antenna", + "anticipo", + "aperto", + "apostolo", + "appalto", + "appello", + "appiglio", + "applauso", + "appoggio", + "appurare", + "aprile", + "aquila", + "arabo", + "arachidi", + "aragosta", + "arancia", + "arbitrio", + "archivio", + "arco", + "argento", + "argilla", + "aria", + "ariete", + "arma", + "armonia", + "aroma", + "arrivare", + "arrosto", + "arsenale", + "arte", + "artiglio", + "asfalto", + "asfissia", + "asino", + "asparagi", + "aspirina", + "assalire", + "assegno", + "assolto", + "assurdo", + "asta", + "astratto", + "atlante", + "atletica", + "atomo", + "atropina", + "attacco", + "attesa", + "attico", + "atto", + "attrarre", + "auguri", + "aula", + "aumento", + "aurora", + "auspicio", + "autista", + "auto", + "autunno", + "avanzare", + "avarizia", + "avere", + "aviatore", + "avido", + "avorio", + "avvenire", + "avviso", + "avvocato", + "azienda", + "azione", + "azzardo", + "azzurro", + "babbuino", + "bacio", + "badante", + "baffi", + "bagaglio", + "bagliore", + "bagno", + "balcone", + "balena", + "ballare", + "balordo", + "balsamo", + "bambola", + "bancomat", + "banda", + "barato", + "barba", + "barista", + "barriera", + "basette", + "basilico", + "bassista", + "bastare", + "battello", + "bavaglio", + "beccare", + "beduino", + "bellezza", + "bene", + "benzina", + "berretto", + "bestia", + "bevitore", + "bianco", + "bibbia", + "biberon", + "bibita", + "bici", + "bidone", + "bilancia", + "biliardo", + "binario", + "binocolo", + "biologia", + "biondina", + "biopsia", + "biossido", + "birbante", + "birra", + "biscotto", + "bisogno", + "bistecca", + "bivio", + "blindare", + "bloccare", + "bocca", + "bollire", + "bombola", + "bonifico", + "borghese", + "borsa", + "bottino", + "botulino", + "braccio", + "bradipo", + "branco", + "bravo", + "bresaola", + "bretelle", + "brevetto", + "briciola", + "brigante", + "brillare", + "brindare", + "brivido", + "broccoli", + "brontolo", + "bruciare", + "brufolo", + "bucare", + "buddista", + "budino", + "bufera", + "buffo", + "bugiardo", + "buio", + "buono", + "burrone", + "bussola", + "bustina", + "buttare", + "cabernet", + "cabina", + "cacao", + "cacciare", + "cactus", + "cadavere", + "caffe", + "calamari", + "calcio", + "caldaia", + "calmare", + "calunnia", + "calvario", + "calzone", + "cambiare", + "camera", + "camion", + "cammello", + "campana", + "canarino", + "cancello", + "candore", + "cane", + "canguro", + "cannone", + "canoa", + "cantare", + "canzone", + "caos", + "capanna", + "capello", + "capire", + "capo", + "capperi", + "capra", + "capsula", + "caraffa", + "carbone", + "carciofo", + "cardigan", + "carenza", + "caricare", + "carota", + "carrello", + "carta", + "casa", + "cascare", + "caserma", + "cashmere", + "casino", + "cassetta", + "castello", + "catalogo", + "catena", + "catorcio", + "cattivo", + "causa", + "cauzione", + "cavallo", + "caverna", + "caviglia", + "cavo", + "cazzotto", + "celibato", + "cemento", + "cenare", + "centrale", + "ceramica", + "cercare", + "ceretta", + "cerniera", + "certezza", + "cervello", + "cessione", + "cestino", + "cetriolo", + "chiave", + "chiedere", + "chilo", + "chimera", + "chiodo", + "chirurgo", + "chitarra", + "chiudere", + "ciabatta", + "ciao", + "cibo", + "ciccia", + "cicerone", + "ciclone", + "cicogna", + "cielo", + "cifra", + "cigno", + "ciliegia", + "cimitero", + "cinema", + "cinque", + "cintura", + "ciondolo", + "ciotola", + "cipolla", + "cippato", + "circuito", + "cisterna", + "citofono", + "ciuccio", + "civetta", + "civico", + "clausola", + "cliente", + "clima", + "clinica", + "cobra", + "coccole", + "cocktail", + "cocomero", + "codice", + "coesione", + "cogliere", + "cognome", + "colla", + "colomba", + "colpire", + "coltello", + "comando", + "comitato", + "commedia", + "comodino", + "compagna", + "comune", + "concerto", + "condotto", + "conforto", + "congiura", + "coniglio", + "consegna", + "conto", + "convegno", + "coperta", + "copia", + "coprire", + "corazza", + "corda", + "corleone", + "cornice", + "corona", + "corpo", + "corrente", + "corsa", + "cortesia", + "corvo", + "coso", + "costume", + "cotone", + "cottura", + "cozza", + "crampo", + "cratere", + "cravatta", + "creare", + "credere", + "crema", + "crescere", + "crimine", + "criterio", + "croce", + "crollare", + "cronaca", + "crostata", + "croupier", + "cubetto", + "cucciolo", + "cucina", + "cultura", + "cuoco", + "cuore", + "cupido", + "cupola", + "cura", + "curva", + "cuscino", + "custode", + "danzare", + "data", + "decennio", + "decidere", + "decollo", + "dedicare", + "dedurre", + "definire", + "delegare", + "delfino", + "delitto", + "demone", + "dentista", + "denuncia", + "deposito", + "derivare", + "deserto", + "designer", + "destino", + "detonare", + "dettagli", + "diagnosi", + "dialogo", + "diamante", + "diario", + "diavolo", + "dicembre", + "difesa", + "digerire", + "digitare", + "diluvio", + "dinamica", + "dipinto", + "diploma", + "diramare", + "dire", + "dirigere", + "dirupo", + "discesa", + "disdetta", + "disegno", + "disporre", + "dissenso", + "distacco", + "dito", + "ditta", + "diva", + "divenire", + "dividere", + "divorare", + "docente", + "dolcetto", + "dolore", + "domatore", + "domenica", + "dominare", + "donatore", + "donna", + "dorato", + "dormire", + "dorso", + "dosaggio", + "dottore", + "dovere", + "download", + "dragone", + "dramma", + "dubbio", + "dubitare", + "duetto", + "durata", + "ebbrezza", + "eccesso", + "eccitare", + "eclissi", + "economia", + "edera", + "edificio", + "editore", + "edizione", + "educare", + "effetto", + "egitto", + "egiziano", + "elastico", + "elefante", + "eleggere", + "elemento", + "elenco", + "elezione", + "elmetto", + "elogio", + "embrione", + "emergere", + "emettere", + "eminenza", + "emisfero", + "emozione", + "empatia", + "energia", + "enfasi", + "enigma", + "entrare", + "enzima", + "epidemia", + "epilogo", + "episodio", + "epoca", + "equivoco", + "erba", + "erede", + "eroe", + "erotico", + "errore", + "eruzione", + "esaltare", + "esame", + "esaudire", + "eseguire", + "esempio", + "esigere", + "esistere", + "esito", + "esperto", + "espresso", + "essere", + "estasi", + "esterno", + "estrarre", + "eterno", + "etica", + "euforico", + "europa", + "evacuare", + "evasione", + "evento", + "evidenza", + "evitare", + "evolvere", + "fabbrica", + "facciata", + "fagiano", + "fagotto", + "falco", + "fame", + "famiglia", + "fanale", + "fango", + "fantasia", + "farfalla", + "farmacia", + "faro", + "fase", + "fastidio", + "faticare", + "fatto", + "favola", + "febbre", + "femmina", + "femore", + "fenomeno", + "fermata", + "feromoni", + "ferrari", + "fessura", + "festa", + "fiaba", + "fiamma", + "fianco", + "fiat", + "fibbia", + "fidare", + "fieno", + "figa", + "figlio", + "figura", + "filetto", + "filmato", + "filosofo", + "filtrare", + "finanza", + "finestra", + "fingere", + "finire", + "finta", + "finzione", + "fiocco", + "fioraio", + "firewall", + "firmare", + "fisico", + "fissare", + "fittizio", + "fiume", + "flacone", + "flagello", + "flirtare", + "flusso", + "focaccia", + "foglio", + "fognario", + "follia", + "fonderia", + "fontana", + "forbici", + "forcella", + "foresta", + "forgiare", + "formare", + "fornace", + "foro", + "fortuna", + "forzare", + "fosforo", + "fotoni", + "fracasso", + "fragola", + "frantumi", + "fratello", + "frazione", + "freccia", + "freddo", + "frenare", + "fresco", + "friggere", + "frittata", + "frivolo", + "frizione", + "fronte", + "frullato", + "frumento", + "frusta", + "frutto", + "fucile", + "fuggire", + "fulmine", + "fumare", + "funzione", + "fuoco", + "furbizia", + "furgone", + "furia", + "furore", + "fusibile", + "fuso", + "futuro", + "gabbiano", + "galassia", + "gallina", + "gamba", + "gancio", + "garanzia", + "garofano", + "gasolio", + "gatto", + "gazebo", + "gazzetta", + "gelato", + "gemelli", + "generare", + "genitori", + "gennaio", + "geologia", + "germania", + "gestire", + "gettare", + "ghepardo", + "ghiaccio", + "giaccone", + "giaguaro", + "giallo", + "giappone", + "giardino", + "gigante", + "gioco", + "gioiello", + "giorno", + "giovane", + "giraffa", + "giudizio", + "giurare", + "giusto", + "globo", + "gloria", + "glucosio", + "gnocca", + "gocciola", + "godere", + "gomito", + "gomma", + "gonfiare", + "gorilla", + "governo", + "gradire", + "graffiti", + "granchio", + "grappolo", + "grasso", + "grattare", + "gridare", + "grissino", + "grondaia", + "grugnito", + "gruppo", + "guadagno", + "guaio", + "guancia", + "guardare", + "gufo", + "guidare", + "guscio", + "gusto", + "icona", + "idea", + "identico", + "idolo", + "idoneo", + "idrante", + "idrogeno", + "igiene", + "ignoto", + "imbarco", + "immagine", + "immobile", + "imparare", + "impedire", + "impianto", + "importo", + "impresa", + "impulso", + "incanto", + "incendio", + "incidere", + "incontro", + "incrocia", + "incubo", + "indagare", + "indice", + "indotto", + "infanzia", + "inferno", + "infinito", + "infranto", + "ingerire", + "inglese", + "ingoiare", + "ingresso", + "iniziare", + "innesco", + "insalata", + "inserire", + "insicuro", + "insonnia", + "insulto", + "interno", + "introiti", + "invasori", + "inverno", + "invito", + "invocare", + "ipnosi", + "ipocrita", + "ipotesi", + "ironia", + "irrigare", + "iscritto", + "isola", + "ispirare", + "isterico", + "istinto", + "istruire", + "italiano", + "jazz", + "labbra", + "labrador", + "ladro", + "lago", + "lamento", + "lampone", + "lancetta", + "lanterna", + "lapide", + "larva", + "lasagne", + "lasciare", + "lastra", + "latte", + "laurea", + "lavagna", + "lavorare", + "leccare", + "legare", + "leggere", + "lenzuolo", + "leone", + "lepre", + "letargo", + "lettera", + "levare", + "levitare", + "lezione", + "liberare", + "libidine", + "libro", + "licenza", + "lievito", + "limite", + "lince", + "lingua", + "liquore", + "lire", + "listino", + "litigare", + "litro", + "locale", + "lottare", + "lucciola", + "lucidare", + "luglio", + "luna", + "macchina", + "madama", + "madre", + "maestro", + "maggio", + "magico", + "maglione", + "magnolia", + "mago", + "maialino", + "maionese", + "malattia", + "male", + "malloppo", + "mancare", + "mandorla", + "mangiare", + "manico", + "manopola", + "mansarda", + "mantello", + "manubrio", + "manzo", + "mappa", + "mare", + "margine", + "marinaio", + "marmotta", + "marocco", + "martello", + "marzo", + "maschera", + "matrice", + "maturare", + "mazzetta", + "meandri", + "medaglia", + "medico", + "medusa", + "megafono", + "melone", + "membrana", + "menta", + "mercato", + "meritare", + "merluzzo", + "mese", + "mestiere", + "metafora", + "meteo", + "metodo", + "mettere", + "miele", + "miglio", + "miliardo", + "mimetica", + "minatore", + "minuto", + "miracolo", + "mirtillo", + "missile", + "mistero", + "misura", + "mito", + "mobile", + "moda", + "moderare", + "moglie", + "molecola", + "molle", + "momento", + "moneta", + "mongolia", + "monologo", + "montagna", + "morale", + "morbillo", + "mordere", + "mosaico", + "mosca", + "mostro", + "motivare", + "moto", + "mulino", + "mulo", + "muovere", + "muraglia", + "muscolo", + "museo", + "musica", + "mutande", + "nascere", + "nastro", + "natale", + "natura", + "nave", + "navigare", + "negare", + "negozio", + "nemico", + "nero", + "nervo", + "nessuno", + "nettare", + "neutroni", + "neve", + "nevicare", + "nicotina", + "nido", + "nipote", + "nocciola", + "noleggio", + "nome", + "nonno", + "norvegia", + "notare", + "notizia", + "nove", + "nucleo", + "nuda", + "nuotare", + "nutrire", + "obbligo", + "occhio", + "occupare", + "oceano", + "odissea", + "odore", + "offerta", + "officina", + "offrire", + "oggetto", + "oggi", + "olfatto", + "olio", + "oliva", + "ombelico", + "ombrello", + "omuncolo", + "ondata", + "onore", + "opera", + "opinione", + "opuscolo", + "opzione", + "orario", + "orbita", + "orchidea", + "ordine", + "orecchio", + "orgasmo", + "orgoglio", + "origine", + "orologio", + "oroscopo", + "orso", + "oscurare", + "ospedale", + "ospite", + "ossigeno", + "ostacolo", + "ostriche", + "ottenere", + "ottimo", + "ottobre", + "ovest", + "pacco", + "pace", + "pacifico", + "padella", + "pagare", + "pagina", + "pagnotta", + "palazzo", + "palestra", + "palpebre", + "pancetta", + "panfilo", + "panino", + "pannello", + "panorama", + "papa", + "paperino", + "paradiso", + "parcella", + "parente", + "parlare", + "parodia", + "parrucca", + "partire", + "passare", + "pasta", + "patata", + "patente", + "patogeno", + "patriota", + "pausa", + "pazienza", + "peccare", + "pecora", + "pedalare", + "pelare", + "pena", + "pendenza", + "penisola", + "pennello", + "pensare", + "pentirsi", + "percorso", + "perdono", + "perfetto", + "perizoma", + "perla", + "permesso", + "persona", + "pesare", + "pesce", + "peso", + "petardo", + "petrolio", + "pezzo", + "piacere", + "pianeta", + "piastra", + "piatto", + "piazza", + "piccolo", + "piede", + "piegare", + "pietra", + "pigiama", + "pigliare", + "pigrizia", + "pilastro", + "pilota", + "pinguino", + "pioggia", + "piombo", + "pionieri", + "piovra", + "pipa", + "pirata", + "pirolisi", + "piscina", + "pisolino", + "pista", + "pitone", + "piumino", + "pizza", + "plastica", + "platino", + "poesia", + "poiana", + "polaroid", + "polenta", + "polimero", + "pollo", + "polmone", + "polpetta", + "poltrona", + "pomodoro", + "pompa", + "popolo", + "porco", + "porta", + "porzione", + "possesso", + "postino", + "potassio", + "potere", + "poverino", + "pranzo", + "prato", + "prefisso", + "prelievo", + "premio", + "prendere", + "prestare", + "pretesa", + "prezzo", + "primario", + "privacy", + "problema", + "processo", + "prodotto", + "profeta", + "progetto", + "promessa", + "pronto", + "proposta", + "proroga", + "prossimo", + "proteina", + "prova", + "prudenza", + "pubblico", + "pudore", + "pugilato", + "pulire", + "pulsante", + "puntare", + "pupazzo", + "puzzle", + "quaderno", + "qualcuno", + "quarzo", + "quercia", + "quintale", + "rabbia", + "racconto", + "radice", + "raffica", + "ragazza", + "ragione", + "rammento", + "ramo", + "rana", + "randagio", + "rapace", + "rapinare", + "rapporto", + "rasatura", + "ravioli", + "reagire", + "realista", + "reattore", + "reazione", + "recitare", + "recluso", + "record", + "recupero", + "redigere", + "regalare", + "regina", + "regola", + "relatore", + "reliquia", + "remare", + "rendere", + "reparto", + "resina", + "resto", + "rete", + "retorica", + "rettile", + "revocare", + "riaprire", + "ribadire", + "ribelle", + "ricambio", + "ricetta", + "richiamo", + "ricordo", + "ridurre", + "riempire", + "riferire", + "riflesso", + "righello", + "rilancio", + "rilevare", + "rilievo", + "rimanere", + "rimborso", + "rinforzo", + "rinuncia", + "riparo", + "ripetere", + "riposare", + "ripulire", + "risalita", + "riscatto", + "riserva", + "riso", + "rispetto", + "ritaglio", + "ritmo", + "ritorno", + "ritratto", + "rituale", + "riunione", + "riuscire", + "riva", + "robotica", + "rondine", + "rosa", + "rospo", + "rosso", + "rotonda", + "rotta", + "roulotte", + "rubare", + "rubrica", + "ruffiano", + "rumore", + "ruota", + "ruscello", + "sabbia", + "sacco", + "saggio", + "sale", + "salire", + "salmone", + "salto", + "salutare", + "salvia", + "sangue", + "sanzioni", + "sapere", + "sapienza", + "sarcasmo", + "sardine", + "sartoria", + "sbalzo", + "sbarcare", + "sberla", + "sborsare", + "scadenza", + "scafo", + "scala", + "scambio", + "scappare", + "scarpa", + "scatola", + "scelta", + "scena", + "sceriffo", + "scheggia", + "schiuma", + "sciarpa", + "scienza", + "scimmia", + "sciopero", + "scivolo", + "sclerare", + "scolpire", + "sconto", + "scopa", + "scordare", + "scossa", + "scrivere", + "scrupolo", + "scuderia", + "scultore", + "scuola", + "scusare", + "sdraiare", + "secolo", + "sedativo", + "sedere", + "sedia", + "segare", + "segreto", + "seguire", + "semaforo", + "seme", + "senape", + "seno", + "sentiero", + "separare", + "sepolcro", + "sequenza", + "serata", + "serpente", + "servizio", + "sesso", + "seta", + "settore", + "sfamare", + "sfera", + "sfidare", + "sfiorare", + "sfogare", + "sgabello", + "sicuro", + "siepe", + "sigaro", + "silenzio", + "silicone", + "simbiosi", + "simpatia", + "simulare", + "sinapsi", + "sindrome", + "sinergia", + "sinonimo", + "sintonia", + "sirena", + "siringa", + "sistema", + "sito", + "smalto", + "smentire", + "smontare", + "soccorso", + "socio", + "soffitto", + "software", + "soggetto", + "sogliola", + "sognare", + "soldi", + "sole", + "sollievo", + "solo", + "sommario", + "sondare", + "sonno", + "sorpresa", + "sorriso", + "sospiro", + "sostegno", + "sovrano", + "spaccare", + "spada", + "spagnolo", + "spalla", + "sparire", + "spavento", + "spazio", + "specchio", + "spedire", + "spegnere", + "spendere", + "speranza", + "spessore", + "spezzare", + "spiaggia", + "spiccare", + "spiegare", + "spiffero", + "spingere", + "sponda", + "sporcare", + "spostare", + "spremuta", + "spugna", + "spumante", + "spuntare", + "squadra", + "squillo", + "staccare", + "stadio", + "stagione", + "stallone", + "stampa", + "stancare", + "starnuto", + "statura", + "stella", + "stendere", + "sterzo", + "stilista", + "stimolo", + "stinco", + "stiva", + "stoffa", + "storia", + "strada", + "stregone", + "striscia", + "studiare", + "stufa", + "stupendo", + "subire", + "successo", + "sudare", + "suono", + "superare", + "supporto", + "surfista", + "sussurro", + "svelto", + "svenire", + "sviluppo", + "svolta", + "svuotare", + "tabacco", + "tabella", + "tabu", + "tacchino", + "tacere", + "taglio", + "talento", + "tangente", + "tappeto", + "tartufo", + "tassello", + "tastiera", + "tavolo", + "tazza", + "teatro", + "tedesco", + "telaio", + "telefono", + "tema", + "temere", + "tempo", + "tendenza", + "tenebre", + "tensione", + "tentare", + "teologia", + "teorema", + "termica", + "terrazzo", + "teschio", + "tesi", + "tesoro", + "tessera", + "testa", + "thriller", + "tifoso", + "tigre", + "timbrare", + "timido", + "tinta", + "tirare", + "tisana", + "titano", + "titolo", + "toccare", + "togliere", + "topolino", + "torcia", + "torrente", + "tovaglia", + "traffico", + "tragitto", + "training", + "tramonto", + "transito", + "trapezio", + "trasloco", + "trattore", + "trazione", + "treccia", + "tregua", + "treno", + "triciclo", + "tridente", + "trilogia", + "tromba", + "troncare", + "trota", + "trovare", + "trucco", + "tubo", + "tulipano", + "tumulto", + "tunisia", + "tuono", + "turista", + "tuta", + "tutelare", + "tutore", + "ubriaco", + "uccello", + "udienza", + "udito", + "uffa", + "umanoide", + "umore", + "unghia", + "unguento", + "unicorno", + "unione", + "universo", + "uomo", + "uragano", + "uranio", + "urlare", + "uscire", + "utente", + "utilizzo", + "vacanza", + "vacca", + "vaglio", + "vagonata", + "valle", + "valore", + "valutare", + "valvola", + "vampiro", + "vaniglia", + "vanto", + "vapore", + "variante", + "vasca", + "vaselina", + "vassoio", + "vedere", + "vegetale", + "veglia", + "veicolo", + "vela", + "veleno", + "velivolo", + "velluto", + "vendere", + "venerare", + "venire", + "vento", + "veranda", + "verbo", + "verdura", + "vergine", + "verifica", + "vernice", + "vero", + "verruca", + "versare", + "vertebra", + "vescica", + "vespaio", + "vestito", + "vesuvio", + "veterano", + "vetro", + "vetta", + "viadotto", + "viaggio", + "vibrare", + "vicenda", + "vichingo", + "vietare", + "vigilare", + "vigneto", + "villa", + "vincere", + "violino", + "vipera", + "virgola", + "virtuoso", + "visita", + "vita", + "vitello", + "vittima", + "vivavoce", + "vivere", + "viziato", + "voglia", + "volare", + "volpe", + "volto", + "volume", + "vongole", + "voragine", + "vortice", + "votare", + "vulcano", + "vuotare", + "zabaione", + "zaffiro", + "zainetto", + "zampa", + "zanzara", + "zattera", + "zavorra", + "zenzero", + "zero", + "zingaro", + "zittire", + "zoccolo", + "zolfo", + "zombie", + "zucchero" + ]; +} diff --git a/cw_wownero/lib/mnemonics/japanese.dart b/cw_wownero/lib/mnemonics/japanese.dart index 5d17fdb14..13fc27058 100644 --- a/cw_wownero/lib/mnemonics/japanese.dart +++ b/cw_wownero/lib/mnemonics/japanese.dart @@ -1627,4 +1627,4 @@ class JapaneseMnemonics { "びじゅつかん", "ひしょ" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/portuguese.dart b/cw_wownero/lib/mnemonics/portuguese.dart index bdd63d3b2..e8da10362 100644 --- a/cw_wownero/lib/mnemonics/portuguese.dart +++ b/cw_wownero/lib/mnemonics/portuguese.dart @@ -1627,4 +1627,4 @@ class PortugueseMnemonics { "zenite", "zumbi" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/russian.dart b/cw_wownero/lib/mnemonics/russian.dart index f10af0ff6..1f211847b 100644 --- a/cw_wownero/lib/mnemonics/russian.dart +++ b/cw_wownero/lib/mnemonics/russian.dart @@ -1627,4 +1627,4 @@ class RussianMnemonics { "ячейка", "ящик" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mnemonics/spanish.dart b/cw_wownero/lib/mnemonics/spanish.dart index 531eafd35..106a2ef44 100644 --- a/cw_wownero/lib/mnemonics/spanish.dart +++ b/cw_wownero/lib/mnemonics/spanish.dart @@ -1627,4 +1627,4 @@ class SpanishMnemonics { "ritmo", "rito" ]; -} \ No newline at end of file +} diff --git a/cw_wownero/lib/mywownero.dart b/cw_wownero/lib/mywownero.dart index afbb9b375..eb2a0948d 100644 --- a/cw_wownero/lib/mywownero.dart +++ b/cw_wownero/lib/mywownero.dart @@ -31,12 +31,9 @@ String mnemonicDecode(String seed) { wlist.removeLast(); for (var i = 0; i < wlist.length; i += 3) { - final w1 = - tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); - final w2 = tructWords(englistWordSet) - .indexOf(wlist[i + 1].substring(0, prefixLength)); - final w3 = tructWords(englistWordSet) - .indexOf(wlist[i + 2].substring(0, prefixLength)); + final w1 = tructWords(englistWordSet).indexOf(wlist[i].substring(0, prefixLength)); + final w2 = tructWords(englistWordSet).indexOf(wlist[i + 1].substring(0, prefixLength)); + final w3 = tructWords(englistWordSet).indexOf(wlist[i + 2].substring(0, prefixLength)); if (w1 == -1 || w2 == -1 || w3 == -1) { printV("invalid word in mnemonic"); diff --git a/cw_wownero/lib/pending_wownero_transaction.dart b/cw_wownero/lib/pending_wownero_transaction.dart index 1d1f55138..f5c3b448c 100644 --- a/cw_wownero/lib/pending_wownero_transaction.dart +++ b/cw_wownero/lib/pending_wownero_transaction.dart @@ -48,7 +48,7 @@ class PendingWowneroTransaction with PendingTransaction { rethrow; } } - + @override Future> commitUR() { throw UnimplementedError(); diff --git a/cw_wownero/lib/wownero_subaddress_list.dart b/cw_wownero/lib/wownero_subaddress_list.dart index 876e4bfcf..4790c1ea8 100644 --- a/cw_wownero/lib/wownero_subaddress_list.dart +++ b/cw_wownero/lib/wownero_subaddress_list.dart @@ -56,14 +56,13 @@ abstract class WowneroSubaddressListBase with Store { final address = s.address; final label = s.label; final id = s.addressIndex; - final hasDefaultAddressName = - label.toLowerCase() == 'Primary account'.toLowerCase() || - label.toLowerCase() == 'Untitled account'.toLowerCase(); + final hasDefaultAddressName = label.toLowerCase() == 'Primary account'.toLowerCase() || + label.toLowerCase() == 'Untitled account'.toLowerCase(); final isPrimaryAddress = id == 0 && hasDefaultAddressName; return Subaddress( id: id, address: address, - balance: (s.received/1e12).toStringAsFixed(6), + balance: (s.received / 1e12).toStringAsFixed(6), txCount: s.txCount, label: isPrimaryAddress ? 'Primary address' @@ -139,33 +138,27 @@ abstract class WowneroSubaddressListBase with Store { } } - return allAddresses - .map((s) { - final id = s.addressIndex; - final address = s.address; - final label = s.label; - return Subaddress( - id: id, - address: address, - balance: (s.received/1e12).toStringAsFixed(6), - txCount: s.txCount, - label: id == 0 && - label.toLowerCase() == 'Primary account'.toLowerCase() - ? 'Primary address' - : label); - }) - .toList(); + return allAddresses.map((s) { + final id = s.addressIndex; + final address = s.address; + final label = s.label; + return Subaddress( + id: id, + address: address, + balance: (s.received / 1e12).toStringAsFixed(6), + txCount: s.txCount, + label: id == 0 && label.toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : label); + }).toList(); } Future _newSubaddress({required int accountIndex, required String label}) async { await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); - return subaddress_list - .getAllSubaddresses() - .where((s) { - final address = s.address; - return !_usedAddresses.contains(address); - }) - .isNotEmpty; + return subaddress_list.getAllSubaddresses().where((s) { + final address = s.address; + return !_usedAddresses.contains(address); + }).isNotEmpty; } } diff --git a/cw_wownero/lib/wownero_transaction_history.dart b/cw_wownero/lib/wownero_transaction_history.dart index c4d2d1e81..edca58aad 100644 --- a/cw_wownero/lib/wownero_transaction_history.dart +++ b/cw_wownero/lib/wownero_transaction_history.dart @@ -5,11 +5,10 @@ import 'package:cw_wownero/wownero_transaction_info.dart'; part 'wownero_transaction_history.g.dart'; -class WowneroTransactionHistory = WowneroTransactionHistoryBase - with _$WowneroTransactionHistory; +class WowneroTransactionHistory = WowneroTransactionHistoryBase with _$WowneroTransactionHistory; -abstract class WowneroTransactionHistoryBase - extends TransactionHistoryBase with Store { +abstract class WowneroTransactionHistoryBase extends TransactionHistoryBase + with Store { WowneroTransactionHistoryBase() { transactions = ObservableMap(); } @@ -18,11 +17,9 @@ abstract class WowneroTransactionHistoryBase Future save() async {} @override - void addOne(WowneroTransactionInfo transaction) => - transactions[transaction.id] = transaction; + void addOne(WowneroTransactionInfo transaction) => transactions[transaction.id] = transaction; @override void addMany(Map transactions) => this.transactions.addAll(transactions); - } diff --git a/cw_wownero/lib/wownero_transaction_info.dart b/cw_wownero/lib/wownero_transaction_info.dart index aafdc98e0..13df42684 100644 --- a/cw_wownero/lib/wownero_transaction_info.dart +++ b/cw_wownero/lib/wownero_transaction_info.dart @@ -8,10 +8,9 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_wownero/api/transaction_history.dart'; class WowneroTransactionInfo extends TransactionInfo { - WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date, - this.isPending, this.amount, this.accountIndex, this.addressIndex, this.fee, - this.confirmations) : - id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; + WowneroTransactionInfo(this.txHash, this.height, this.direction, this.date, this.isPending, + this.amount, this.accountIndex, this.addressIndex, this.fee, this.confirmations) + : id = "${txHash}_${amount}_${accountIndex}_${addressIndex}"; WowneroTransactionInfo.fromMap(Map map) : id = "${map['hash']}_${map['amount']}_${map['accountIndex']}_${map['addressIndex']}", diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index d1e0538d1..8c5eab714 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -54,7 +54,10 @@ abstract class WowneroWalletBase extends WalletBase with Store { WowneroWalletBase( - {required WalletInfo walletInfo, required DerivationInfo derivationInfo, required Box unspentCoinsInfo, required String password}) + {required WalletInfo walletInfo, + required DerivationInfo derivationInfo, + required Box unspentCoinsInfo, + required String password}) : balance = ObservableMap.of({ CryptoCurrency.wow: WowneroBalance( fullBalance: @@ -144,8 +147,8 @@ abstract class WowneroWalletBase passphrase: wownero_wallet.getPassphrase()); int? get restoreHeight => - transactionHistory.transactions.values.firstOrNull?.height ?? wownero.Wallet_getRefreshFromBlockHeight(wptr!); - + transactionHistory.transactions.values.firstOrNull?.height ?? + wownero.Wallet_getRefreshFromBlockHeight(wptr!); wownero_wallet.SyncListener? _listener; ReactionDisposer? _onAccountChangeReaction; @@ -635,8 +638,7 @@ abstract class WowneroWalletBase wownero_wallet.getSubaddressLabel(accountIndex, addressIndex); Future> _getAllTransactionsOfAccount(int? accountIndex) async => - (await transaction_history - .getAllTransactions()) + (await transaction_history.getAllTransactions()) .map( (row) => WowneroTransactionInfo( row.hash, diff --git a/cw_wownero/lib/wownero_wallet_addresses.dart b/cw_wownero/lib/wownero_wallet_addresses.dart index e6c6b2b2c..9558ec194 100644 --- a/cw_wownero/lib/wownero_wallet_addresses.dart +++ b/cw_wownero/lib/wownero_wallet_addresses.dart @@ -34,25 +34,26 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @override String get latestAddress { - var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; - var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1; + var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex); while (hiddenAddresses.contains(address)) { addressIndex++; - address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex); } return address; } @override String get addressForExchange { - var addressIndex = subaddress_list.numSubaddresses(account?.id??0) - 1; - var address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + var addressIndex = subaddress_list.numSubaddresses(account?.id ?? 0) - 1; + var address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex); while (hiddenAddresses.contains(address) || manualAddresses.contains(address)) { addressIndex++; - address = getAddress(accountIndex: account?.id??0, addressIndex: addressIndex); + address = getAddress(accountIndex: account?.id ?? 0, addressIndex: addressIndex); } return address; } + @observable Account? account; @@ -69,7 +70,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { @override Future init() async { accountList.update(); - account = accountList.accounts.isEmpty ? Account(id: 0, label: "Primary address") : accountList.accounts.first; + account = accountList.accounts.isEmpty + ? Account(id: 0, label: "Primary address") + : accountList.accounts.first; updateSubaddressList(accountIndex: account?.id ?? 0); await updateAddressesInBox(); } @@ -143,7 +146,9 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { accountIndex: accountIndex, defaultLabel: defaultLabel, usedAddresses: usedAddresses.toList()); - subaddress = (subaddressList.subaddresses.isEmpty) ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) : subaddressList.subaddresses.last; + subaddress = (subaddressList.subaddresses.isEmpty) + ? Subaddress(id: 0, address: address, label: defaultLabel, balance: '0', txCount: 0) + : subaddressList.subaddresses.last; address = subaddress!.address; } @@ -152,6 +157,5 @@ abstract class WowneroWalletAddressesBase extends WalletAddresses with Store { addressInfos[account?.id ?? 0]?.any((it) => it.address == address) ?? false; @override - PaymentURI getPaymentUri(String amount) => - WowneroURI(address: address, amount: amount); + PaymentURI getPaymentUri(String amount) => WowneroURI(address: address, amount: amount); } diff --git a/cw_wownero/lib/wownero_wallet_service.dart b/cw_wownero/lib/wownero_wallet_service.dart index c42f7d60c..4fb8ffa79 100644 --- a/cw_wownero/lib/wownero_wallet_service.dart +++ b/cw_wownero/lib/wownero_wallet_service.dart @@ -23,7 +23,11 @@ import 'package:monero/wownero.dart' as wownero; class WowneroNewWalletCredentials extends WalletCredentials { WowneroNewWalletCredentials( - {required String name, required this.language, required this.isPolyseed, this.passphrase, String? password}) + {required String name, + required this.language, + required this.isPolyseed, + this.passphrase, + String? password}) : super(name: name, password: password); final String language; @@ -33,7 +37,11 @@ class WowneroNewWalletCredentials extends WalletCredentials { class WowneroRestoreWalletFromSeedCredentials extends WalletCredentials { WowneroRestoreWalletFromSeedCredentials( - {required String name, required this.mnemonic, required this.passphrase, int height = 0, String? password}) + {required String name, + required this.mnemonic, + required this.passphrase, + int height = 0, + String? password}) : super(name: name, password: password, height: height); final String mnemonic; @@ -97,9 +105,15 @@ class WowneroWalletService extends WalletService< } await wownero_wallet_manager.createWallet( - path: path, password: credentials.password!, language: credentials.language, passphrase: credentials.passphrase??''); + path: path, + password: credentials.password!, + language: credentials.language, + passphrase: credentials.passphrase ?? ''); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -138,7 +152,11 @@ class WowneroWalletService extends WalletService< throw Exception('Wallet not found'); } - wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password); + wallet = WowneroWallet( + walletInfo: walletInfo, + derivationInfo: await walletInfo.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); throw WalletDeprecationException(seed: wallet.seed, curr: wallet.currency); final isValid = wallet.walletAddresses.validate(); @@ -224,7 +242,11 @@ class WowneroWalletService extends WalletService< if (currentWalletInfo == null) { throw Exception('Wallet not found'); } - final currentWallet = WowneroWallet(walletInfo: currentWalletInfo, derivationInfo: await currentWalletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password); + final currentWallet = WowneroWallet( + walletInfo: currentWalletInfo, + derivationInfo: await currentWalletInfo.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); await currentWallet.renameWalletFiles(newName); @@ -249,7 +271,10 @@ class WowneroWalletService extends WalletService< viewKey: credentials.viewKey, spendKey: credentials.spendKey); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -283,7 +308,10 @@ class WowneroWalletService extends WalletService< seed: credentials.mnemonic, restoreHeight: credentials.height!); final wallet = WowneroWallet( - walletInfo: credentials.walletInfo!, derivationInfo: await credentials.walletInfo!.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: credentials.password!); + walletInfo: credentials.walletInfo!, + derivationInfo: await credentials.walletInfo!.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: credentials.password!); await wallet.init(); return wallet; @@ -303,7 +331,8 @@ class WowneroWalletService extends WalletService< final polyseed = Polyseed.decode(credentials.mnemonic, lang, polyseedCoin); return _restoreFromPolyseed( - path, credentials.password!, polyseed, credentials.walletInfo!, lang, passphrase: credentials.passphrase); + path, credentials.password!, polyseed, credentials.walletInfo!, lang, + passphrase: credentials.passphrase); } catch (e) { // TODO: Implement Exception for wallet list service. printV('WowneroWalletsManager Error: $e'); @@ -313,20 +342,19 @@ class WowneroWalletService extends WalletService< Future _restoreFromPolyseed( String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, - {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, int? overrideHeight, String? passphrase}) async { - - - if (polyseed.isEncrypted == false && - (passphrase??'') != "") { + {PolyseedCoin coin = PolyseedCoin.POLYSEED_WOWNERO, + int? overrideHeight, + String? passphrase}) async { + if (polyseed.isEncrypted == false && (passphrase ?? '') != "") { // Fallback to the different passphrase offset method, when a passphrase // was provided but the polyseed is not encrypted. wownero_wallet_manager.restoreWalletFromPolyseedWithOffset( - path: path, - password: password, - seed: polyseed.encode(lang, coin), - seedOffset: passphrase??'', - language: "English"); - + path: path, + password: password, + seed: polyseed.encode(lang, coin), + seedOffset: passphrase ?? '', + language: "English"); + final wallet = WowneroWallet( walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), @@ -357,9 +385,13 @@ class WowneroWalletService extends WalletService< spendKey: spendKey); wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.seed", value: seed); - wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase??''); + wownero.Wallet_setCacheAttribute(wptr!, key: "cakewallet.passphrase", value: passphrase ?? ''); - final wallet = WowneroWallet(walletInfo: walletInfo, derivationInfo: await walletInfo.getDerivationInfo(), unspentCoinsInfo: unspentCoinsInfoSource, password: password); + final wallet = WowneroWallet( + walletInfo: walletInfo, + derivationInfo: await walletInfo.getDerivationInfo(), + unspentCoinsInfo: unspentCoinsInfoSource, + password: password); await wallet.init(); return wallet; diff --git a/integration_test/helpers/mocks.dart b/integration_test/helpers/mocks.dart index 01259bcc8..b36ff314f 100644 --- a/integration_test/helpers/mocks.dart +++ b/integration_test/helpers/mocks.dart @@ -8,18 +8,20 @@ import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:hive/hive.dart'; import 'package:mocktail/mocktail.dart'; -class MockAppStore extends Mock implements AppStore{} -class MockAuthService extends Mock implements AuthService{} -class MockSettingsStore extends Mock implements SettingsStore {} -class MockAuthenticationStore extends Mock implements AuthenticationStore{} -class MockWalletListStore extends Mock implements WalletListStore{} +class MockAppStore extends Mock implements AppStore {} +class MockAuthService extends Mock implements AuthService {} +class MockSettingsStore extends Mock implements SettingsStore {} -class MockLinkViewModel extends Mock implements LinkViewModel {} +class MockAuthenticationStore extends Mock implements AuthenticationStore {} + +class MockWalletListStore extends Mock implements WalletListStore {} + +class MockLinkViewModel extends Mock implements LinkViewModel {} class MockHiveInterface extends Mock implements HiveInterface {} class MockHiveBox extends Mock implements Box {} -class MockSecureStorage extends Mock implements SecureStorage{} \ No newline at end of file +class MockSecureStorage extends Mock implements SecureStorage {} diff --git a/integration_test/robots/disclaimer_page_robot.dart b/integration_test/robots/disclaimer_page_robot.dart index 4b6ca85ce..282169a9c 100644 --- a/integration_test/robots/disclaimer_page_robot.dart +++ b/integration_test/robots/disclaimer_page_robot.dart @@ -34,7 +34,7 @@ class DisclaimerPageRobot { Future tapAcceptButton() async { await commonTestCases.tapItemByKey('disclaimer_accept_button_key'); - + await commonTestCases.defaultSleepTime(); } } diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart index 17197116a..5b35329e1 100644 --- a/integration_test/robots/restore_from_seed_or_key_robot.dart +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -66,7 +66,7 @@ class RestoreFromSeedOrKeysPageRobot { Future enterSeedPhraseForWalletRestore(String text) async { ValidatableAnnotatedEditableTextState seedTextState = - await tester.state(find.byType(ValidatableAnnotatedEditableText)); + await tester.state(find.byType(ValidatableAnnotatedEditableText)); seedTextState.widget.controller.text = text; await tester.pumpAndSettle(); diff --git a/integration_test/robots/security_and_backup_page_robot.dart b/integration_test/robots/security_and_backup_page_robot.dart index b03db5ac6..9bb4c7d44 100644 --- a/integration_test/robots/security_and_backup_page_robot.dart +++ b/integration_test/robots/security_and_backup_page_robot.dart @@ -21,7 +21,7 @@ class SecurityAndBackupPageRobot { Future navigateToShowKeysPage() async { await tester.pumpAndSettle(Duration(milliseconds: 1000)); - + await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key'); await tester.pumpAndSettle(Duration(milliseconds: 1000)); } diff --git a/integration_test/test_suites/confirm_seeds_flow_test.dart b/integration_test/test_suites/confirm_seeds_flow_test.dart index 60adf95e1..121f0315a 100644 --- a/integration_test/test_suites/confirm_seeds_flow_test.dart +++ b/integration_test/test_suites/confirm_seeds_flow_test.dart @@ -26,7 +26,7 @@ void main() { (tester) async { // Store the original FlutterError.onError handler final originalOnError = FlutterError.onError; - + FlutterError.onError = (FlutterErrorDetails details) { debugPrint('FlutterError caught: ${details.exception}'); }; diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart index 90baab9cc..0b876eff1 100644 --- a/integration_test/test_suites/exchange_flow_test.dart +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -25,7 +25,7 @@ void main() { FlutterError.onError = (FlutterErrorDetails details) { debugPrint('FlutterError caught: ${details.exception}'); }; - + commonTestFlows = CommonTestFlows(tester); exchangePageRobot = ExchangePageRobot(tester); dashboardPageRobot = DashboardPageRobot(tester); diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart index cbb8deceb..d10233a34 100644 --- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart +++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/cake_pay/src/widgets/denominations_amount_widget.dar import 'package:cake_wallet/cake_pay/src/widgets/enter_amount_widget.dart'; import 'package:cake_wallet/cake_pay/src/widgets/image_placeholder.dart'; import 'package:cake_wallet/cake_pay/src/widgets/link_extractor.dart'; +import 'package:cake_wallet/cake_pay/src/widgets/prepaid_range_amount_widget.dart'; import 'package:cake_wallet/cake_pay/src/widgets/rounded_overlay_cards_widget.dart'; import 'package:cake_wallet/cake_pay/src/widgets/text_icon_button.dart'; import 'package:cake_wallet/cake_pay/src/widgets/three_checkbox_alert_content_widget.dart'; @@ -147,24 +148,38 @@ class CakePayBuyCardPage extends BasePage { ), bottomCardChild: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: card.denominationItems.isNotEmpty - ? DenominationsAmountWidget( - fiatCurrency: card.fiatCurrency.title, - denominations: card.denominationItems, - amountFieldFocus: _amountFieldFocus, - amountController: _amountController, - quantityFieldFocus: _quantityFieldFocus, - quantityController: _quantityController, - onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, - onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged, - cakePayBuyCardViewModel: cakePayBuyCardViewModel) - : EnterAmountWidget( - minValue: card.minValue ?? '-', - maxValue: card.maxValue ?? '-', - fiatCurrency: card.fiatCurrency.title, - amountFieldFocus: _amountFieldFocus, - amountController: _amountController, - onAmountChanged: cakePayBuyCardViewModel.onAmountChanged))), + child: Column(children: [ + if (card.prepaidRange.isNotEmpty) + PrepaidRangeAmountWidget( + fiatCurrency: card.fiatCurrency.title, + prepaidRanges: card.prepaidRange, + amountFieldFocus: _amountFieldFocus, + amountController: _amountController, + cakePayBuyCardViewModel: cakePayBuyCardViewModel, + onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, + ) + else if (card.denominationItems.isNotEmpty) + DenominationsAmountWidget( + fiatCurrency: card.fiatCurrency.title, + denominations: card.denominationItems, + amountFieldFocus: _amountFieldFocus, + amountController: _amountController, + quantityFieldFocus: _quantityFieldFocus, + quantityController: _quantityController, + onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, + onQuantityChanged: cakePayBuyCardViewModel.onQuantityChanged, + cakePayBuyCardViewModel: cakePayBuyCardViewModel, + ) + else + EnterAmountWidget( + minValue: card.minValue ?? '-', + maxValue: card.maxValue ?? '-', + fiatCurrency: card.fiatCurrency.title, + amountFieldFocus: _amountFieldFocus, + amountController: _amountController, + onAmountChanged: cakePayBuyCardViewModel.onAmountChanged, + ), + ]))), Expanded( flex: 2, child: Padding( diff --git a/lib/cake_pay/src/models/cake_pay_card.dart b/lib/cake_pay/src/models/cake_pay_card.dart index 97a73c224..ea37a310e 100644 --- a/lib/cake_pay/src/models/cake_pay_card.dart +++ b/lib/cake_pay/src/models/cake_pay_card.dart @@ -1,8 +1,21 @@ +import 'package:cake_wallet/core/utilities.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +enum CakePayCardType { + prepaid('Prepaid Cards'), + gift('gift'), + onDemand('on_demand'), + custom('custom'); + + const CakePayCardType(this.apiValue); + + final String apiValue; +} + class CakePayCard { final int id; final String name; + final CakePayCardType? type; final String? description; final String? termsAndConditions; final String? howToUse; @@ -15,26 +28,77 @@ class CakePayCard { final String? minValue; final String? maxValue; final List denominationItems; + final List prepaidRange; - CakePayCard({ - required this.id, - required this.name, - this.description, - this.termsAndConditions, - this.howToUse, - this.expiryAndValidity, - this.cardImageUrl, - this.country, - required this.fiatCurrency, - this.minValueUsd, - this.maxValueUsd, - this.minValue, - this.maxValue, + CakePayCard( + {required this.id, + required this.name, + this.type, + this.description, + this.termsAndConditions, + this.howToUse, + this.expiryAndValidity, + this.cardImageUrl, + this.country, + required this.fiatCurrency, + this.minValueUsd, + this.maxValueUsd, + this.minValue, + this.maxValue, + List? denominationItems, + List? prepaidRange}) + : denominationItems = denominationItems ?? const [], + prepaidRange = prepaidRange ?? const []; + + CakePayCard copyWith({ + int? id, + String? name, + CakePayCardType? type, + String? description, + String? termsAndConditions, + String? howToUse, + String? expiryAndValidity, + String? cardImageUrl, + String? country, + FiatCurrency? fiatCurrency, + String? minValueUsd, + String? maxValueUsd, + String? minValue, + String? maxValue, List? denominationItems, - }) : denominationItems = denominationItems ?? const []; + List? prepaidRange, + }) { + return CakePayCard( + id: id ?? this.id, + name: name ?? this.name, + type: type ?? this.type, + description: description ?? this.description, + termsAndConditions: termsAndConditions ?? this.termsAndConditions, + howToUse: howToUse ?? this.howToUse, + expiryAndValidity: expiryAndValidity ?? this.expiryAndValidity, + cardImageUrl: cardImageUrl ?? this.cardImageUrl, + country: country ?? this.country, + fiatCurrency: fiatCurrency ?? this.fiatCurrency, + minValueUsd: minValueUsd ?? this.minValueUsd, + maxValueUsd: maxValueUsd ?? this.maxValueUsd, + minValue: minValue ?? this.minValue, + maxValue: maxValue ?? this.maxValue, + denominationItems: denominationItems ?? this.denominationItems, + prepaidRange: prepaidRange ?? this.prepaidRange, + ); + } + + static CakePayCardType? cakePayCardTypeFromApi(String? value) { + if (value == null) return null; + final normalized = value.trim().toLowerCase(); + return CakePayCardType.values.firstWhereOrNull( + (e) => e.apiValue.toLowerCase() == normalized, + ); + } factory CakePayCard.fromJson(Map json) { final name = stripHtmlIfNeeded(json['name'] as String? ?? ''); + final typeString = json['type'] as String? ?? ''; final description = stripHtmlIfNeeded(json['description'] as String? ?? ''); final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? ''); final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? ''); @@ -63,9 +127,12 @@ class CakePayCard { } } + final cakePayCardType = cakePayCardTypeFromApi(typeString); + return CakePayCard( id: json['id'] as int? ?? 0, name: name, + type: cakePayCardType, description: description, termsAndConditions: termsAndConditions, howToUse: howToUse, @@ -103,12 +170,6 @@ class Denomination { this.usdValue, }); - static double? _toDouble(dynamic v) { - if (v == null) return null; - if (v is num) return v.toDouble(); - return double.tryParse(v.toString()); - } - factory Denomination.fromJson(Map json) { return Denomination( value: _toDouble(json['value']) ?? 0, @@ -117,3 +178,29 @@ class Denomination { ); } } + +class PrepaidRange { + final double minValue; + final double maxValue; + final String rangeId; + + PrepaidRange({ + required this.minValue, + required this.maxValue, + required this.rangeId, + }); + + factory PrepaidRange.fromCard(CakePayCard card) { + return PrepaidRange( + minValue: _toDouble(card.minValue) ?? 0, + maxValue: _toDouble(card.maxValue) ?? 0, + rangeId: card.id.toString(), + ); + } +} + +double? _toDouble(dynamic v) { + if (v == null) return null; + if (v is num) return v.toDouble(); + return double.tryParse(v.toString()); +} diff --git a/lib/cake_pay/src/models/cake_pay_vendor.dart b/lib/cake_pay/src/models/cake_pay_vendor.dart index 2f2f865d4..c31fad0b0 100644 --- a/lib/cake_pay/src/models/cake_pay_vendor.dart +++ b/lib/cake_pay/src/models/cake_pay_vendor.dart @@ -31,9 +31,26 @@ class CakePayVendor { if (cardsJson != null && cardsJson.isNotEmpty) { try { - cardForVendor = CakePayCard.fromJson(cardsJson.firstWhere((card) { - return Country.normalizeName(card['country'] as String) == country; - }) as Map); + final cards = cardsJson + .map((cardJson) => CakePayCard.fromJson(cardJson as Map)) + .toList(); + final cardsForSelectedCountry = + cards.where((card) => Country.normalizeName(card.country ?? '') == country).toList(); + + if (cardsForSelectedCountry.isNotEmpty) { + final isPrepaid = + cardsForSelectedCountry.every((card) => card.type == CakePayCardType.prepaid); + + if (isPrepaid) { + final firstCard = cardsForSelectedCountry.first; + final prepaidRanges = cardsForSelectedCountry.map(PrepaidRange.fromCard).toList() + ..sort((a, b) => a.minValue.compareTo(b.minValue)); + + cardForVendor = firstCard.copyWith(prepaidRange: prepaidRanges); + } else { + cardForVendor = cardsForSelectedCountry.first; + } + } } catch (e) { printV('Error parsing card for vendor: $e'); } diff --git a/lib/cake_pay/src/services/cake_pay_api.dart b/lib/cake_pay/src/services/cake_pay_api.dart index 13aa1b7d5..84cc1f4df 100644 --- a/lib/cake_pay/src/services/cake_pay_api.dart +++ b/lib/cake_pay/src/services/cake_pay_api.dart @@ -8,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/utils/print_verbose.dart'; class CakePayApi { - static const testBaseUri = FeatureFlag.hasDevOptions; + static const testBaseUri = false; //FeatureFlag.hasDevOptions; static const baseTestCakePayUri = 'api-stg.cakepay.com'; static const baseProdCakePayUri = 'api-prod.cakepay.com'; diff --git a/lib/cake_pay/src/widgets/prepaid_range_amount_widget.dart b/lib/cake_pay/src/widgets/prepaid_range_amount_widget.dart new file mode 100644 index 000000000..7914e893e --- /dev/null +++ b/lib/cake_pay/src/widgets/prepaid_range_amount_widget.dart @@ -0,0 +1,134 @@ +import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart'; +import 'package:cake_wallet/view_model/cake_pay/cake_pay_buy_card_view_model.dart'; +import 'package:flutter/material.dart'; + +import 'enter_amount_widget.dart'; + +class PrepaidRangeAmountWidget extends StatefulWidget { + const PrepaidRangeAmountWidget({ + required this.fiatCurrency, + required this.prepaidRanges, + required this.amountFieldFocus, + required this.amountController, + required this.cakePayBuyCardViewModel, + required this.onAmountChanged, + }); + + final String fiatCurrency; + final List prepaidRanges; + final FocusNode amountFieldFocus; + final TextEditingController amountController; + final CakePayBuyCardViewModel cakePayBuyCardViewModel; + final Function(String) onAmountChanged; + + @override + State createState() => _PrepaidRangeAmountWidgetState(); +} + +class _PrepaidRangeAmountWidgetState extends State { + late PrepaidRange _selectedRange; + + @override + void initState() { + super.initState(); + _selectedRange = widget.prepaidRanges.first; + _applySelectedRange(forceAmountUpdate: widget.amountController.text.isEmpty); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Select range:', + style: TextStyle( + color: Theme.of(context).textTheme.titleLarge!.color!, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + value: _selectedRange, + isExpanded: true, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + filled: true, + fillColor: Theme.of(context).cardColor, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.onSurface.withAlpha(40), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + items: widget.prepaidRanges + .map( + (range) => DropdownMenuItem( + value: range, + child: Text(_rangeLabel(range)), + ), + ) + .toList(), + onChanged: (range) { + if (range == null) return; + setState(() => _selectedRange = range); + widget.cakePayBuyCardViewModel.onPrepaidRangeChanged(range); + _applySelectedRange(forceAmountUpdate: true); + }, + ), + const SizedBox(height: 12), + EnterAmountWidget( + minValue: _formatAmount(_selectedRange.minValue), + maxValue: _formatAmount(_selectedRange.maxValue), + fiatCurrency: widget.fiatCurrency, + amountFieldFocus: widget.amountFieldFocus, + amountController: widget.amountController, + onAmountChanged: (value) { + widget.onAmountChanged(value); + widget.cakePayBuyCardViewModel.selectedDenomination = ( + value.replaceAll(',', '.'), + int.tryParse(_selectedRange.rangeId), + ); + }, + ), + ], + ); + } + + void _applySelectedRange({required bool forceAmountUpdate}) { + final currentAmount = double.tryParse(widget.amountController.text); + final isCurrentAmountInRange = currentAmount != null && + currentAmount >= _selectedRange.minValue && + currentAmount <= _selectedRange.maxValue; + + final nextAmount = forceAmountUpdate || !isCurrentAmountInRange + ? _formatAmount(_selectedRange.minValue) + : widget.amountController.text; + + widget.amountController.text = nextAmount; + widget.onAmountChanged(nextAmount); + widget.cakePayBuyCardViewModel.selectedDenomination = ( + nextAmount.replaceAll(',', '.'), + int.tryParse(_selectedRange.rangeId), + ); + } + + String _rangeLabel(PrepaidRange range) { + return '${widget.fiatCurrency} ${_formatAmount(range.minValue)} - ${_formatAmount(range.maxValue)}'; + } + + String _formatAmount(double value) { + return value % 1 == 0 ? value.toInt().toString() : value.toStringAsFixed(2); + } +} diff --git a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart b/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart index fecf11b93..a4361db1a 100644 --- a/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart +++ b/lib/cake_pay/src/widgets/rounded_overlay_cards_widget.dart @@ -12,32 +12,46 @@ class RoundedOverlayCards extends StatelessWidget { @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; + return ClipRRect( borderRadius: BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), child: Container( - height: screenHeight * 0.50, decoration: BoxDecoration( - borderRadius: BorderRadius.only( + borderRadius: constBorderRadius.only( bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24), ), - color: Theme.of(context).colorScheme.surfaceContainer), + color: Theme.of(context).colorScheme.surfaceContainer, + ), child: Column( + mainAxisSize: MainAxisSize.min, children: [ - ClipRRect( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)), + Flexible( + child: ClipRRect( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(25.0), + bottomRight: Radius.circular(25.0), + ), child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), - color: Theme.of(context).colorScheme.surfaceContainerLow, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(24), + bottomRight: Radius.circular(24), ), - height: screenHeight * 0.38, - width: double.infinity, - child: topCardChild)), - bottomCardChild, + color: Theme.of(context).colorScheme.surfaceContainerLow, + ), + constraints: BoxConstraints( + maxHeight: screenHeight * 0.38, + ), + width: double.infinity, + child: topCardChild, + ), + ), + ), + Flexible( + child: bottomCardChild, + ), ], ), ), diff --git a/lib/di.dart b/lib/di.dart index 05954e5df..9f6b100c8 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -423,16 +423,19 @@ Future setup({ case HardwareWalletType.seedsigner: case HardwareWalletType.keystone: throw Exception( - "This should not have happened, because airgapped Wallets don't need View Models"); + "This should not have happened, because airgapped Wallets don't need View Models", + ); } }); - getIt.registerLazySingleton(() => LedgerViewModel()); + getIt.registerLazySingleton(() => LedgerViewModel(getIt())); - getIt.registerLazySingleton(() => BitboxViewModel()); + getIt.registerLazySingleton(BitboxViewModel.new); getIt.registerLazySingleton( - () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet")); + () => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"), + ); + getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt())); getIt.registerFactory(() => KeyService(getIt.get())); diff --git a/lib/evm/cw_evm.dart b/lib/evm/cw_evm.dart index 0b28b7ee0..a291169ee 100644 --- a/lib/evm/cw_evm.dart +++ b/lib/evm/cw_evm.dart @@ -166,12 +166,15 @@ class CWEVM extends EVM { amount: amount, fee: fee, tokenSymbol: tokenSymbol, + exponent: exponent, direction: direction, isPending: isPending, date: date, confirmations: confirmations, to: to, from: from, + evmSignatureName: evmSignatureName, + contractAddress: contractAddress, chainId: chainId); @override diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index e6ddabaec..1551b95de 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -433,6 +433,9 @@ class CWMonero extends Monero { @override void setGlobalLedgerConnection(ledger.LedgerConnection connection) => gLedger = connection; + @override + bool hasGlobalLedgerConnection() => gLedger != null; + @override String? getLastLedgerCommand() => latestLedgerCommand; diff --git a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart index b987c11e2..d36d67bf3 100644 --- a/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart +++ b/lib/new-ui/widgets/coins_page/assets_history/assets_history_section.dart @@ -49,6 +49,7 @@ class _AssetsHistorySectionState extends State { final oldTabLength = tabs.length; final hasAssetsTab = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled || (widget.dashboardViewModel.hasMweb && widget.dashboardViewModel.mwebEnabled); + final hasAssetsButton = widget.dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled; final hasNftTab = isNFTACtivatedChain(widget.dashboardViewModel.wallet.type, chainId: widget.dashboardViewModel.wallet.chainId); tabs = [ @@ -58,13 +59,14 @@ class _AssetsHistorySectionState extends State { AssetsSection( dashboardViewModel: widget.dashboardViewModel, ), + hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg", () { Navigator.of(context).pushNamed( Routes.homeSettings, arguments: widget.dashboardViewModel.balanceViewModel, ); - })), + }) : null), AssetsHistorySectionTab( S.current.history, HistorySection( diff --git a/lib/new-ui/widgets/receive_page/receive_top_bar.dart b/lib/new-ui/widgets/receive_page/receive_top_bar.dart index 8fb2d0eb5..91bf87154 100644 --- a/lib/new-ui/widgets/receive_page/receive_top_bar.dart +++ b/lib/new-ui/widgets/receive_page/receive_top_bar.dart @@ -12,7 +12,6 @@ class ModalTopBar extends StatelessWidget { this.onTrailingPressed = nothing, this.leadingIcon, this.trailingIcon, - this.bottomText, this.padding, this.leadingWidget, this.trailingWidget}) { diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 49a3c61c9..02ddd5639 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -75,6 +75,23 @@ void startAuthenticationStateChange( ); return; } + + ledgerVM.suppressMoneroReconnectPrompt = true; + + if (!monero!.hasGlobalLedgerConnection()) { + await showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: "Unexpected Error", + alertContent: + "Unexpected Error while trying to connect to the device (has no global LedgerConnection)", + buttonText: S.of(context).try_again, + buttonAction: Navigator.of(context).pop, + ), + ); + return; + } + showPopUp( context: context, builder: (context) => AlertWithOneAction( @@ -85,14 +102,17 @@ void startAuthenticationStateChange( buttonAction: () => Navigator.of(context).pop(), ), ); - bool tryOpening = true; + + var isConnected = false; + var tryOpening = true; while (tryOpening) { try { await loadCurrentWallet(); + isConnected = true; tryOpening = false; } on Exception catch (e) { final ledgerErrorMessage = ledgerVM.interpretErrorCode(e.toString()); - if (ledgerErrorMessage != null) { + if (ledgerErrorMessage != null && context.mounted) { await showPopUp( context: context, builder: (context) => AlertWithTwoActions( @@ -116,16 +136,17 @@ void startAuthenticationStateChange( } getIt.get().showNext(); - await navigatorKey.currentState! - .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + if (isConnected) { + ledgerVM.suppressMoneroReconnectPrompt = false; + await navigatorKey.currentState! + .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + } }, allowChangeWallet: true, ), ); - - // await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.connectDevices, (route) => false, arguments: ConnectDevicePageParams(walletType: walletType, onConnectDevice: onConnectDevice)); } else { - navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + await navigatorKey.currentState!.pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); } WidgetsBinding.instance.addPostFrameCallback((_) { final linkViewModel = getIt.get(); diff --git a/lib/solana/cw_solana.dart b/lib/solana/cw_solana.dart index f1450331b..d552ed11c 100644 --- a/lib/solana/cw_solana.dart +++ b/lib/solana/cw_solana.dart @@ -122,9 +122,13 @@ class CWSolana extends Solana { return CryptoCurrency.sol; } - return (wallet as SolanaWallet).splTokenCurrencies.firstWhere( - (element) => transaction.amount.currency.symbol == element.symbol, - ); + final token = (wallet as SolanaWallet).splTokenBySymbol(transaction.amount.currency.symbol); + + if (token == null) { + throw StateError('No SPL token for symbol ${transaction.amount.currency.symbol}'); + } + + return token; } @override @@ -401,6 +405,15 @@ class CWSolana extends Solana { } await Future.wait(tokenChecks); + + final discoveredMints = result.newTokens + .where((item) => item.token.enabled) + .map((item) => item.token.mintAddress) + .toList(); + + if (discoveredMints.isNotEmpty) { + await wallet.updateSPLTokenTransactions(specificMints: discoveredMints); + } } catch (_) {} } diff --git a/lib/src/screens/connect_device/connect_device_page.dart b/lib/src/screens/connect_device/connect_device_page.dart index c96ec27bc..870134dc7 100644 --- a/lib/src/screens/connect_device/connect_device_page.dart +++ b/lib/src/screens/connect_device/connect_device_page.dart @@ -1,54 +1,54 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/src/widgets/primary_button.dart'; -import 'package:cake_wallet/themes/core/material_base_theme.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/new-ui/widgets/new_primary_button.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/base_page.dart"; +import "package:cake_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart"; +import "package:cake_wallet/src/widgets/cake_image_widget.dart"; +import "package:cake_wallet/src/widgets/primary_button.dart"; +import "package:cake_wallet/themes/core/material_base_theme.dart"; +import "package:cake_wallet/utils/responsive_layout_util.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:flutter_mobx/flutter_mobx.dart"; typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel); class ConnectDevicePageParams { - final WalletType walletType; - final OnConnectDevice onConnectDevice; - final bool allowChangeWallet; - final bool isReconnect; - final HardwareWalletType hardwareWalletType; - - ConnectDevicePageParams({ + const ConnectDevicePageParams({ required this.walletType, required this.hardwareWalletType, required this.onConnectDevice, this.allowChangeWallet = false, this.isReconnect = true, }); -} -class ConnectDevicePage extends BasePage { final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final bool isReconnect; - final HardwareWalletViewModel hardwareWalletVM; + final HardwareWalletType hardwareWalletType; +} +class ConnectDevicePage extends BasePage { ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM) : walletType = params.walletType, onConnectDevice = params.onConnectDevice, allowChangeWallet = params.allowChangeWallet, isReconnect = params.isReconnect; + final WalletType walletType; + final OnConnectDevice onConnectDevice; + final bool allowChangeWallet; + final bool isReconnect; + final HardwareWalletViewModel hardwareWalletVM; @override String get title => isReconnect @@ -60,31 +60,32 @@ class ConnectDevicePage extends BasePage { @override Widget body(BuildContext context) => PopScope( - canPop: !isReconnect, - child: ConnectDevicePageBody( - walletType, - onConnectDevice, - allowChangeWallet, - hardwareWalletVM, - currentTheme, - )); + canPop: !isReconnect, + child: ConnectDevicePageBody( + walletType, + onConnectDevice, + hardwareWalletVM, + currentTheme, + allowChangeWallet: allowChangeWallet, + ), + ); } class ConnectDevicePageBody extends StatefulWidget { + const ConnectDevicePageBody( + this.walletType, + this.onConnectDevice, + this.hardwareWalletVM, + this.currentTheme, { + this.allowChangeWallet = false, + }); + final WalletType walletType; final OnConnectDevice onConnectDevice; final bool allowChangeWallet; final HardwareWalletViewModel hardwareWalletVM; final MaterialThemeBase currentTheme; - const ConnectDevicePageBody( - this.walletType, - this.onConnectDevice, - this.allowChangeWallet, - this.hardwareWalletVM, - this.currentTheme, - ); - @override ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState(); } @@ -95,10 +96,10 @@ class ConnectDevicePageBodyState extends State { List get allDevices => [...bleDevices, ...usbDevices]; - late Timer? _usbRefreshTimer = null; - late Timer? _bleRefreshTimer = null; - late Timer? _bleStateTimer = null; - late StreamSubscription? _bleRefresh = null; + Timer? _usbRefreshTimer; + Timer? _bleRefreshTimer; + Timer? _bleStateTimer; + StreamSubscription? _bleRefresh; bool longWait = false; Timer? _longWaitTimer; @@ -107,19 +108,22 @@ class ConnectDevicePageBodyState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - _bleStateTimer = - Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState()); + _bleStateTimer = Timer.periodic( + const Duration(seconds: 1), + (_) => widget.hardwareWalletVM.updateBleState(), + ); - _bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices()); + _bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices()); if (Platform.isAndroid) { - _usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices()); + _usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices()); } if (widget.hardwareWalletVM.hasBluetooth) { - _longWaitTimer = Timer(Duration(seconds: 10), () { - if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) + _longWaitTimer = Timer(const Duration(seconds: 10), () { + if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) { setState(() => longWait = true); + } }); } }); @@ -134,22 +138,29 @@ class ConnectDevicePageBodyState extends State { _longWaitTimer?.cancel(); widget.hardwareWalletVM.stopScanning(); + _isConnectPressed = false; super.dispose(); } Future _refreshUsbDevices() async { final dev = await widget.hardwareWalletVM.getAllUsbDevices(); - if (usbDevices.length != dev.length) setState(() => usbDevices = dev); + + if (usbDevices.length != dev.length) { + setState(() => usbDevices = dev); + } } Future _refreshBleDevices() async { try { if (widget.hardwareWalletVM.isBleEnabled) { - _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() { - bleDevices.add(device); - if (longWait) longWait = false; - })) - ..onError((Object e) { + _bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen( + (device) => setState(() { + bleDevices.add(device); + if (longWait) { + longWait = false; + } + }), + )..onError((e) { printV(e); }); _bleRefreshTimer?.cancel(); @@ -160,48 +171,67 @@ class ConnectDevicePageBodyState extends State { } } + var _isConnectPressed = false; + Future _connectToDevice(HardwareWalletDevice device) async { - final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + if (_isConnectPressed) { + return; + } + + _isConnectPressed = true; + try { + final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType); + _isConnectPressed = false; WidgetsBinding.instance.addPostFrameCallback((_) { if (isConnected) - widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); - }); + { + widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM); + } + }); + } catch (_) { + _isConnectPressed = false; + } } String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) { + printV(deviceType); switch (deviceType) { case HardwareWalletDeviceType.ledgerNanoX: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; case HardwareWalletDeviceType.ledgerNanoGen5: - return 'assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg"; case HardwareWalletDeviceType.ledgerStax: - return 'assets/new-ui/hardware_wallets/ledger_stax.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_stax.svg"; case HardwareWalletDeviceType.ledgerFlex: - return 'assets/new-ui/hardware_wallets/ledger_flex.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_flex.svg"; case HardwareWalletDeviceType.BitBox02: case HardwareWalletDeviceType.BitBox02Nova: - return 'assets/new-ui/hardware_wallets/device_bitbox.svg'; + return "assets/new-ui/hardware_wallets/device_bitbox.svg"; case HardwareWalletDeviceType.trezorModelOne: - return 'assets/new-ui/hardware_wallets/device_trezor_model_one.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg"; case HardwareWalletDeviceType.trezorModelT: - return 'assets/new-ui/hardware_wallets/device_trezor_model_t.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg"; case HardwareWalletDeviceType.trezorSafe3: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_3.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg"; case HardwareWalletDeviceType.trezorSafe5: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_5.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg"; case HardwareWalletDeviceType.trezorSafe7: - return 'assets/new-ui/hardware_wallets/device_trezor_safe_7.svg'; + return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg"; default: - return 'assets/new-ui/hardware_wallets/ledger_nano_x.svg'; + return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg"; } } String get description { - if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble; + if (!Platform.isAndroid) { + return S.of(context).connect_your_hardware_wallet_ble; + } - if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet; + if (widget.hardwareWalletVM.hasBluetooth) { + return S.of(context).connect_your_hardware_wallet; + } return S.of(context).connect_your_hardware_wallet_usb; } @@ -210,7 +240,7 @@ class ConnectDevicePageBodyState extends State { Widget build(BuildContext context) => Center( child: Container( width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint, - padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24), + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24), child: Column( children: [ Expanded( @@ -218,9 +248,7 @@ class ConnectDevicePageBodyState extends State { child: Column( spacing: 24, children: [ - CakeImageWidget( - imageUrl: "assets/new-ui/hardware_wallet.svg", - ), + const CakeImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"), Text( description, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), @@ -231,7 +259,7 @@ class ConnectDevicePageBodyState extends State { offstage: widget.hardwareWalletVM.isBleEnabled || !widget.hardwareWalletVM.hasBluetooth, child: Padding( - padding: EdgeInsets.only(left: 20, right: 20, bottom: 20), + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20), child: Text( S.of(context).ledger_please_enable_bluetooth, style: Theme.of(context).textTheme.titleMedium, @@ -242,67 +270,89 @@ class ConnectDevicePageBodyState extends State { ), Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainer, - borderRadius: BorderRadius.circular(18)), + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(18), + ), child: ListView.separated( - shrinkWrap: true, - itemBuilder: (context, index) { - final item = allDevices[index]; - return GestureDetector( - onTap: () => _connectToDevice(item), - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: 48, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - spacing: 12, - children: [ - CakeImageWidget( - imageUrl: _getDeviceTileLeading(item.type), - width: 24, - height: 24, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn), - ), - Text(item.name) - ], - ), - Row( - spacing: 12, - children: [ - if (item.connectionType == - HardwareWalletConnectionType.ble) - CakeImageWidget( - imageUrl: "assets/new-ui/bluetooth.svg", + shrinkWrap: true, + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + final item = allDevices[index]; + return GestureDetector( + onTap: () => _connectToDevice(item), + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 48, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + spacing: 12, + children: [ + Observer( + builder: (_) { + if (widget.hardwareWalletVM.isConnecting) { + return CupertinoActivityIndicator( + animating: true, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + radius: 12, + ); + } + + return CakeImageWidget( + imageUrl: _getDeviceTileLeading(item.type), width: 24, height: 24, colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary, - BlendMode.srcIn), - ), + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ); + }, + ), + Text(item.name), + ], + ), + Row( + spacing: 12, + children: [ + if (item.connectionType == + HardwareWalletConnectionType.ble) CakeImageWidget( - imageUrl: "assets/new-ui/arrow_forward.svg", - height: 16, - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.onSurfaceVariant, - BlendMode.srcIn)) - ], - ) - ], - ), + imageUrl: "assets/new-ui/bluetooth.svg", + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + ), + CakeImageWidget( + imageUrl: "assets/new-ui/arrow_forward.svg", + height: 16, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, + BlendMode.srcIn, + ), + ), + ], + ), + ], ), ), - ); - }, - separatorBuilder: (context, index) => Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh), - itemCount: allDevices.length), + ), + ); + }, + separatorBuilder: (context, index) => Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + itemCount: allDevices.length, + ), ), if (widget.allowChangeWallet) ...[ PrimaryButton( @@ -310,7 +360,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.primary, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: _onChangeWallet, - ) + ), ], ], ), @@ -321,7 +371,7 @@ class ConnectDevicePageBodyState extends State { color: Theme.of(context).colorScheme.surfaceContainer, textColor: Theme.of(context).colorScheme.primary, onPressed: () => _onHowToConnect(context), - ) + ), ], ), ), @@ -339,17 +389,25 @@ class ConnectDevicePageBodyState extends State { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet( + builder: (_) => InfoStepsBottomSheet( titleText: S.of(context).how_to_connect, steps: [ - InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg', - S.of(context).connect_hw_info_step_1), - InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg', - S.of(context).connect_hw_info_step_2), - InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg', - S.of(context).connect_hw_info_step_3), - InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg', - S.of(context).connect_hw_info_step_4), + InfoStep( + "assets/images/wallet_connect_step_icons/step1_power.svg", + S.of(context).connect_hw_info_step_1, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step2_connect.svg", + S.of(context).connect_hw_info_step_2, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step3_unlock.svg", + S.of(context).connect_hw_info_step_3, + ), + InfoStep( + "assets/images/wallet_connect_step_icons/step4_select.svg", + S.of(context).connect_hw_info_step_4, + ), ], ), ); diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index b3ac158d9..85a9bbba5 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,25 +1,25 @@ -import 'dart:async'; +import "dart:async"; -import 'package:cake_wallet/core/amount_parsing_proxy.dart'; -import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/preferences_key.dart'; -import 'package:cake_wallet/reactions/wallet_connect.dart'; -import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart'; -import 'package:cake_wallet/themes/core/theme_store.dart'; -import 'package:cake_wallet/utils/exception_handler.dart'; -import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/balance.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/transaction_history.dart'; -import 'package:cake_wallet/store/wallet_list_store.dart'; -import 'package:cake_wallet/store/authentication_store.dart'; -import 'package:cake_wallet/store/settings_store.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:cake_wallet/core/amount_parsing_proxy.dart"; +import "package:cake_wallet/di.dart"; +import "package:cake_wallet/entities/preferences_key.dart"; +import "package:cake_wallet/reactions/wallet_connect.dart"; +import "package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart"; +import "package:cake_wallet/store/authentication_store.dart"; +import "package:cake_wallet/store/settings_store.dart"; +import "package:cake_wallet/store/wallet_list_store.dart"; +import "package:cake_wallet/themes/core/theme_store.dart"; +import "package:cake_wallet/utils/exception_handler.dart"; +import "package:cw_core/balance.dart"; +import "package:cw_core/transaction_history.dart"; +import "package:cw_core/transaction_info.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:mobx/mobx.dart"; +import "package:shared_preferences/shared_preferences.dart"; -part 'app_store.g.dart'; +part "app_store.g.dart"; class AppStore = AppStoreBase with _$AppStore; @@ -59,8 +59,9 @@ abstract class AppStoreBase with Store { @action Future changeCurrentWallet( WalletBase, TransactionInfo> wallet) async { - bool changingToSameWalletType = this.wallet?.type == wallet.type; - this.wallet?.close(shouldCleanup: !changingToSameWalletType); + final changingToSameWalletType = this.wallet?.type == wallet.type; + + await this.wallet?.close(shouldCleanup: !changingToSameWalletType); this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); @@ -80,7 +81,7 @@ abstract class AppStoreBase with Store { wcService.create(); await wcService.init(); } catch (e, s) { - printV('WalletConnect setup failed: $e\n$s'); + printV("WalletConnect setup failed: $e\n$s"); } } } diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index b3393da63..a95e2353d 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -33,8 +33,12 @@ abstract class CakePayBuyCardViewModelBase with Store { : 0, quantity = 1, card = vendor.card!, - min = _toDouble(vendor.card!.minValue) ?? 0, - max = _toDouble(vendor.card!.maxValue) ?? 0 { + min = vendor.card!.prepaidRange.isNotEmpty + ? vendor.card!.prepaidRange.first.minValue + : _toDouble(vendor.card!.minValue) ?? 0, + max = vendor.card!.prepaidRange.isNotEmpty + ? vendor.card!.prepaidRange.first.maxValue + : _toDouble(vendor.card!.maxValue) ?? 0 { selectedPaymentMethod = availableMethods.isNotEmpty ? availableMethods.first : null; } @@ -46,8 +50,6 @@ abstract class CakePayBuyCardViewModelBase with Store { final CakePayVendor vendor; final SendViewModel sendViewModel; final CakePayService _cakePayService; - final double max; - final double min; final CakePayCard card; final WalletType walletType; final Box orders; @@ -59,6 +61,8 @@ abstract class CakePayBuyCardViewModelBase with Store { bool confirmsNoVpn = false; bool confirmsVoidedRefund = false; bool confirmsTermsAgreed = false; + double max; + double min; (String, int?) selectedDenomination = ('', null); String simulatedResponse = ''; @@ -136,10 +140,24 @@ abstract class CakePayBuyCardViewModelBase with Store { @action void onQuantityChanged(int? input) => quantity = input ?? 1; + void onPrepaidRangeChanged(PrepaidRange range) { + min = range.minValue; + max = range.maxValue; + selectedDenomination = (range.minValue.toString(), int.tryParse(range.rangeId)); + + if (amount < min || amount > max) { + amount = min; + } + } + @action void onAmountChanged(String input) { if (input.isEmpty) return; amount = double.parse(input.replaceAll(',', '.')); + + if (card.prepaidRange.isNotEmpty) { + selectedDenomination = (input.replaceAll(',', '.'), selectedDenomination.$2); + } } @action @@ -153,10 +171,25 @@ abstract class CakePayBuyCardViewModelBase with Store { sendViewModel.state = FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.'); } + + final isPrepaidRangeSelected = card.prepaidRange.isNotEmpty; + + int selectedCardId = card.id; + String selectedPrice = amount.toString(); + + if (isPrepaidRangeSelected) { + selectedCardId = selectedDenomination.$2 ?? card.id; + selectedPrice = + selectedDenomination.$1.isNotEmpty ? selectedDenomination.$1 : amount.toString(); + } else if (isDenominationSelected) { + selectedCardId = selectedDenomination.$2 ?? card.id; + selectedPrice = selectedDenomination.$1; + } + try { order = await _cakePayService.createOrder( - cardId: isDenominationSelected ? selectedDenomination.$2 ?? card.id : card.id, - price: isDenominationSelected ? selectedDenomination.$1 : amount.toString(), + cardId: selectedCardId, + price: selectedPrice, quantity: quantity, confirmsNoVpn: confirmsNoVpn, confirmsVoidedRefund: confirmsVoidedRefund, diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 4f098c02f..3128f74d8 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -14,7 +14,6 @@ import 'package:cake_wallet/zano/zano.dart'; import 'package:cw_core/amount/money.dart'; import 'package:cw_core/crypto_amount_format.dart'; import 'package:cw_core/transaction_history.dart'; -import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; @@ -498,7 +497,6 @@ abstract class BalanceViewModelBase with Store { final record = wallet.balance[curr]!; final available = record.available - (record.secondAvailable ?? Money.zero(curr)); final price = fiatConversionStore.prices[curr] ?? 0; - printV(record.available); ret += double.tryParse(calculateFiatAmount(price: price, cryptoAmount: available.toString()) .replaceAll(",", "")) ?? 0; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 4dbc82752..17ffd334a 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -227,9 +227,10 @@ abstract class DashboardViewModelBase with Store { 1; } catch (_) {} } else { - confirmations = appStore.wallet!.transactionHistory.transactions.values - .map((item) => item.isPending) - .fold(0, (val, pending) => pending ? val + 1 : val); + final pendingCount = appStore.wallet!.transactionHistory.transactions.values + .where((item) => item.isPending) + .length; + confirmations = pendingCount + 1; } return length * confirmations; }, _transactionDisposerCallback, delay: 300); @@ -1331,9 +1332,10 @@ abstract class DashboardViewModelBase with Store { 1; } catch (_) {} } else { - confirmations = appStore.wallet!.transactionHistory.transactions.values - .map((item) => item.isPending) - .fold(0, (val, pending) => pending ? val + 1 : val); + final pendingCount = appStore.wallet!.transactionHistory.transactions.values + .where((item) => item.isPending) + .length; + confirmations = pendingCount + 1; } return length * confirmations; }, _transactionDisposerCallback, delay: 300); diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 8676cf86c..4d6a0d527 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -195,7 +195,7 @@ class TransactionListItem extends ActionListItem with Keyable { case WalletType.decred: case WalletType.zcash: amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); case WalletType.ethereum: @@ -206,15 +206,15 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; case WalletType.solana: - final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); + final asset = assetOfTransaction; final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -222,7 +222,7 @@ class TransactionListItem extends ActionListItem with Keyable { final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; @@ -234,7 +234,7 @@ class TransactionListItem extends ActionListItem with Keyable { } final price = balanceViewModel.fiatConversionStore.prices[asset]; amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(transaction.amount.toString()), + cryptoAmount: transaction.amount.toDouble(), price: price, ).withLocalSeperator(_appStore.settingsStore.languageCode); break; diff --git a/lib/view_model/hardware_wallet/bitbox_view_model.dart b/lib/view_model/hardware_wallet/bitbox_view_model.dart index 39c4b5123..a07b63cfb 100644 --- a/lib/view_model/hardware_wallet/bitbox_view_model.dart +++ b/lib/view_model/hardware_wallet/bitbox_view_model.dart @@ -35,6 +35,10 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => Platform.isIOS && false; // TODO: remove when we enable bluetooth @@ -52,10 +56,11 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { Future stopScanning() async {} @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { if (!(device is BitboxHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; try { final appDocDir = await getAppDir(); @@ -66,17 +71,15 @@ abstract class BitboxViewModelBase extends HardwareWalletViewModel with Store { printV("Bitbox initialized!", file: '${appDocDir.path}/error.txt'); await bitboxManager.channelHashVerify(); printV("Bitbox channel-hash verified!", file: '${appDocDir.path}/error.txt'); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } - bool _isConnecting = false; - @override bool isConnected(WalletType type) => false; diff --git a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart index 1b87abff2..ce5473914 100644 --- a/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart +++ b/lib/view_model/hardware_wallet/hardware_wallet_view_model.dart @@ -1,13 +1,14 @@ -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; abstract class HardwareWalletViewModel { HardwareWalletType get hardwareWalletType; bool get isBleEnabled; bool get hasBluetooth; + bool get isConnecting; bool isConnected(WalletType type); @@ -26,4 +27,6 @@ abstract class HardwareWalletViewModel { Future initWallet(WalletBase wallet); String? interpretErrorCode(String error) => null; + + Future close() async {} } diff --git a/lib/view_model/hardware_wallet/ledger_view_model.dart b/lib/view_model/hardware_wallet/ledger_view_model.dart index dc608d8d3..f5efaa52e 100644 --- a/lib/view_model/hardware_wallet/ledger_view_model.dart +++ b/lib/view_model/hardware_wallet/ledger_view_model.dart @@ -1,50 +1,47 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart'; -import 'package:cake_wallet/evm/evm.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/main.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/routes.dart'; -import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; -import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart'; -import 'package:cake_wallet/wallet_type_utils.dart'; -import 'package:cw_core/hardware/device_connection_type.dart'; -import 'package:cw_core/hardware/hardware_wallet_service.dart'; -import 'package:cw_core/utils/print_verbose.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:flutter/widgets.dart'; -import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as sdk; -import 'package:mobx/mobx.dart'; -import 'package:permission_handler/permission_handler.dart'; +import "package:cake_wallet/bitcoin/bitcoin.dart"; +import "package:cake_wallet/entities/hardware_wallet/hardware_wallet_device.dart"; +import "package:cake_wallet/evm/evm.dart"; +import "package:cake_wallet/generated/i18n.dart"; +import "package:cake_wallet/main.dart"; +import "package:cake_wallet/monero/monero.dart"; +import "package:cake_wallet/routes.dart"; +import "package:cake_wallet/src/screens/connect_device/connect_device_page.dart"; +import "package:cake_wallet/store/app_store.dart"; +import "package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart"; +import "package:cake_wallet/wallet_type_utils.dart"; +import "package:cw_core/hardware/device_connection_type.dart"; +import "package:cw_core/hardware/hardware_wallet_service.dart"; +import "package:cw_core/utils/print_verbose.dart"; +import "package:cw_core/wallet_base.dart"; +import "package:cw_core/wallet_info.dart"; +import "package:cw_core/wallet_type.dart"; +import "package:flutter/widgets.dart"; +import "package:ledger_flutter_plus/ledger_flutter_plus.dart" as sdk; +import "package:mobx/mobx.dart"; +import "package:permission_handler/permission_handler.dart"; -part 'ledger_view_model.g.dart'; +part "ledger_view_model.g.dart"; class LedgerViewModel = LedgerViewModelBase with _$LedgerViewModel; abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { - late final sdk.LedgerInterface ledgerPlusBLE; - late final sdk.LedgerInterface ledgerPlusUSB; - - bool get _doesSupportHardwareWallets { - if (isMoneroOnly) { - return DeviceConnectionType.supportedConnectionTypes( - WalletType.monero, HardwareWalletType.ledger, Platform.isIOS) - .isNotEmpty; - } - - return true; - } - - LedgerViewModelBase() { + LedgerViewModelBase(AppStore appStore) { if (_doesSupportHardwareWallets) { reaction((_) => isBleEnabled, (_) { - if (isBleEnabled) _initBLE(); + if (isBleEnabled) { + _initBLE(); + } }); + + reaction((_) => appStore.wallet, (wallet) async { + if (wallet != null && wallet.type != _connectingWalletType) { + await close(); + } + }); + updateBleState(); if (!Platform.isIOS) { @@ -53,6 +50,21 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } } + late final sdk.LedgerInterface ledgerPlusBLE; + late final sdk.LedgerInterface ledgerPlusUSB; + + bool get _doesSupportHardwareWallets { + if (isMoneroOnly) { + return DeviceConnectionType.supportedConnectionTypes( + WalletType.monero, + HardwareWalletType.ledger, + Platform.isIOS, + ).isNotEmpty; + } + + return true; + } + @override HardwareWalletType get hardwareWalletType => HardwareWalletType.ledger; @@ -60,6 +72,10 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { @observable bool isBleEnabled = false; + @override + @observable + bool isConnecting = false; + @override bool get hasBluetooth => true; @@ -69,9 +85,11 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { if (isBleEnabled && !_bleIsInitialized) { ledgerPlusBLE = sdk.LedgerInterface.ble( onPermissionRequest: (_) async { - if (Platform.isMacOS) return true; + if (Platform.isMacOS) { + return true; + } - Map statuses = await [ + final statuses = await [ Permission.bluetoothScan, Permission.bluetoothConnect, Permission.bluetoothAdvertise, @@ -79,7 +97,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return statuses.values.where((status) => status.isDenied).isEmpty; }, - bleOptions: sdk.BluetoothOptions(maxScanDuration: Duration(minutes: 5)), + bleOptions: sdk.BluetoothOptions(maxScanDuration: const Duration(minutes: 5)), ); _bleIsInitialized = true; } @@ -91,28 +109,41 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final newState = bleState == sdk.AvailabilityState.poweredOn; - if (newState != isBleEnabled) isBleEnabled = newState; + if (newState != isBleEnabled) { + isBleEnabled = newState; + } } @override Stream scanForBleDevices() => - ledgerPlusBLE.scan().map((d) => LedgerHardwareWalletDevice(d)); + ledgerPlusBLE.scan().map(LedgerHardwareWalletDevice.new); @override - Future> getAllUsbDevices() => ledgerPlusUSB.devices - .then((devices) => devices.map((d) => LedgerHardwareWalletDevice(d)).toList()); + Future> getAllUsbDevices() => + ledgerPlusUSB.devices.then((devices) => devices.map(LedgerHardwareWalletDevice.new).toList()); @override Future stopScanning() async { - if (_bleIsInitialized) await ledgerPlusBLE.stopScanning(); - if (!Platform.isIOS) await ledgerPlusUSB.stopScanning(); + if (_bleIsInitialized) { + await ledgerPlusBLE.stopScanning(); + } + if (!Platform.isIOS) { + await ledgerPlusUSB.stopScanning(); + } } @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type) async { - if (!(device is LedgerHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (device is! LedgerHardwareWalletDevice) { + return false; + } + + if (isConnecting) { + return false; + } + + isConnecting = true; _connectingWalletType = type; if (isConnected(type)) { try { @@ -123,34 +154,36 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { final ledger = device.connectionType == HardwareWalletConnectionType.ble ? ledgerPlusBLE : ledgerPlusUSB; - if (_connectionChangeSubscription == null) { - _connectionChangeSubscription = - ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); - } + _connectionChangeSubscription ??= + ledger.deviceStateChanges(device.device.id).listen(_connectionChangeListener); try { _connection = await ledger.connect(device.device); - _isConnecting = false; + isConnecting = false; return true; } catch (e) { printV(e); } - _isConnecting = false; + isConnecting = false; return false; } StreamSubscription? _connectionChangeSubscription; sdk.LedgerConnection? _connection; - bool _isConnecting = false; WalletType? _connectingWalletType; + bool suppressMoneroReconnectPrompt = false; void _connectionChangeListener(sdk.BleConnectionState event) { - printV('Ledger Device State Changed: $event'); - if (event == sdk.BleConnectionState.disconnected && !_isConnecting) { + printV("Ledger Device State Changed: $event"); + if (event == sdk.BleConnectionState.disconnected && !isConnecting) { _connection = null; if (_connectingWalletType == WalletType.monero) { monero!.resetLedgerConnection(); + if (suppressMoneroReconnectPrompt) { + return; + } + Navigator.of(navigatorKey.currentContext!).pushNamed( Routes.connectDevices, arguments: ConnectDevicePageParams( @@ -158,7 +191,7 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { hardwareWalletType: HardwareWalletType.ledger, allowChangeWallet: true, isReconnect: true, - onConnectDevice: (context, ledgerVM) async { + onConnectDevice: (context, ledgerVM) { if (context.mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } @@ -170,26 +203,24 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { } @override - bool isConnected(WalletType type) => _connection != null && !(_connection!.isDisconnected); + bool isConnected(WalletType type) => _connection != null && !_connection!.isDisconnected; sdk.LedgerConnection get connection => _connection!; @override - Future initWallet(WalletBase wallet) async { + Future initWallet(WalletBase wallet) { switch (wallet.type) { case WalletType.monero: return monero!.setLedgerConnection(wallet, connection); case WalletType.bitcoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.litecoin: - return bitcoin! - .setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return bitcoin!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); case WalletType.ethereum: case WalletType.polygon: - return evm!.setHardwareWalletService(wallet, await getHardwareWalletService(wallet.type)); + return evm!.setHardwareWalletService(wallet, getHardwareWalletService(wallet.type)); default: - throw Exception('Unexpected wallet type: ${wallet.type} for ledger'); + throw Exception("Unexpected wallet type: ${wallet.type} for ledger"); } } @@ -216,16 +247,31 @@ abstract class LedgerViewModelBase extends HardwareWalletViewModel with Store { return error; } - var errorRegex = RegExp(r'(?:0x\S*?|[0-9a-f]{4})(?= )').firstMatch(error.toString()); + final errorRegex = RegExp(r"(?:0x\S*?|[0-9a-f]{4})(?= )").firstMatch(error.toString()); - String errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; + final errorCode = errorRegex?.group(0).toString().replaceAll("0x", "") ?? ""; if (errorCode.contains("6985")) { return S.current.ledger_error_tx_rejected_by_user; } else if (errorCode.contains("5515")) { return S.current.ledger_error_device_locked; - } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any((e) => errorCode.contains(e))) { + } else if (["6e01", "6a87", "6d02", "6511", "6e00"].any(errorCode.contains)) { return S.current.ledger_error_wrong_app; } return null; } + + @override + Future close() async { + try { + printV("Resetting ledger VM"); + await _connectionChangeSubscription?.cancel(); + await _connection?.disconnect().catchError((_) {}); + + _connectingWalletType = null; + _connectionChangeSubscription = null; + _connection = null; + isConnecting = false; + await stopScanning(); + } catch (_) {} + } } diff --git a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart index 7115cdf5a..c048edf7c 100644 --- a/lib/view_model/hardware_wallet/trezor_connect_view_model.dart +++ b/lib/view_model/hardware_wallet/trezor_connect_view_model.dart @@ -82,6 +82,10 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S @override HardwareWalletType get hardwareWalletType => HardwareWalletType.trezor; + @override + @observable + bool isConnecting = false; + @override @observable bool isBleEnabled = false; @@ -112,7 +116,6 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S if (!Platform.isIOS) await trezorUSB.stopScanning(); } - bool _isConnecting = false; sdk.TrezorClient? _client; Completer? _pinCompleter; @@ -123,11 +126,12 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S void setParingPin(String pin) => _pinCompleter?.complete(pin); @override + @action Future connectDevice(HardwareWalletDevice device, WalletType type, [bool isRetry = false]) async { if (!(device is TrezorHardwareWalletDevice)) return false; - if (_isConnecting) return false; - _isConnecting = true; + if (isConnecting) return false; + isConnecting = true; paringState = TrezorParingState.initial; try { @@ -182,7 +186,7 @@ abstract class TrezorConnectViewModelBase extends HardwareWalletViewModel with S printV(e); return false; } finally { - _isConnecting = false; + isConnecting = false; } } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index a566cab7d..8c49cdc83 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1121,6 +1121,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor // it is required because evm, solana and tron don't actually save the transaction info when you send something. // instead, they rely on the tx to eventually get fetched at sync time, which can take a while if (isEVMWallet) { + final selectedToken = evm!.getERC20Currencies(wallet).firstWhereOrNull( + (token) => token.title.toUpperCase() == selectedCryptoCurrency.title.toUpperCase(), + ); wallet.transactionHistory.addOne(evm!.getTransactionInfo( id: pendingTransaction!.evmTxHashFromRawHex!, height: 0, @@ -1132,6 +1135,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor date: DateTime.now(), confirmations: 0, chainId: wallet.chainId ?? 0, + contractAddress: selectedToken?.contractAddress, )); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index d8ae44e53..78b6793dd 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -77,8 +77,7 @@ abstract class WalletListViewModelBase with Store { if (walletItem.type == WalletType.haven) { return; } - // bool switchingToSameWalletType = walletItem.type == _appStore.wallet?.type; - // await _appStore.wallet?.close(shouldCleanup: !switchingToSameWalletType); + final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); await _appStore.changeCurrentWallet(wallet); updateList(); diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 8cb43a77c..afe2d64be 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -125,7 +125,7 @@ dependencies: ledger_flutter_plus: git: url: https://github.com/vespr-wallet/ledger-flutter-plus - ref: 0ccfbe26298e8974dfd4e891f34719c8348562f9 + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 trezor_connect: git: url: https://github.com/cake-tech/trezor_connect @@ -201,14 +201,8 @@ dependency_overrides: ffi: 2.1.0 ledger_flutter_plus: git: - url: https://github.com/cake-tech/ledger-flutter-plus - ref: 7bcb00380783f8611f68f5cd63fec96261fea2c2 - ledger_usb_plus: - git: - # url: https://github.com/konstantinullrich/ledger-usb-plus - # ref: dad482c847c0d0eedceabbb40d3478e64b65e630 - url: https://github.com/cake-tech/ledger-usb-plus - ref: 734c3d6f8267ba52ad73b6e1e78e570c07298890 + url: https://github.com/vespr-wallet/ledger-flutter-plus + ref: eaf49764ad0863e8dec97ad30ff6aea0a1d1bd28 socks5_proxy: git: url: https://github.com/LacticWhale/socks_dart.git @@ -231,7 +225,7 @@ dependency_overrides: universal_ble: git: url: https://github.com/cake-tech/universal_ble.git - ref: f7044558ad111a156987614971e08c583c80175c + ref: 6735c1bdf7f42c501d51c20564eb957c3a12b981 flutter_icons: image_path: "assets/images/app_logo.png" diff --git a/test/core/amount_parsing_proxy_test.dart b/test/core/amount_parsing_proxy_test.dart index cf228f37c..f6fffd7c2 100644 --- a/test/core/amount_parsing_proxy_test.dart +++ b/test/core/amount_parsing_proxy_test.dart @@ -48,7 +48,8 @@ void main() { group("getCanonicalCryptoAmount", () { test("Amount should not change for Bitcoin", () { - final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc); + final amount = + amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btc); expect(amount, "0.000001"); }); @@ -58,7 +59,8 @@ void main() { }); test("Amount should not change for Bitcoin", () { - final amount = amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln); + final amount = + amountParsingProxy.getCanonicalCryptoAmount("0.000001", CryptoCurrency.btcln); expect(amount, "0.000001"); }); @@ -118,7 +120,8 @@ void main() { }); test("Amount should be formated to Satoshi for Bitcoin Lightning", () { - final amount = amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln); + final amount = + amountParsingProxy.getDisplayCryptoAmount("0.000001", CryptoCurrency.btcln); expect(amount, "100"); }); diff --git a/test/core/universal_address_detector_test.dart b/test/core/universal_address_detector_test.dart index 9ea590127..dc732b7f2 100644 --- a/test/core/universal_address_detector_test.dart +++ b/test/core/universal_address_detector_test.dart @@ -48,7 +48,8 @@ void main() { }); test('detects Lightning invoice', () { - const invoice = 'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n'; + const invoice = + 'lnbc1u1p3zj7j8pp5vkwxc4vxfzgzxcjzk5ksp0l7jgrgzmqxqmexxp8e8gfqk6s0knnqdlc35hsggzypnxw7xxxnrvd3hscqzpgxqrrsssp5txs5g8vkdmdtnm8g5v9m3u094nnxkl0jjf38jj5xn2pptxlqkwn7gpgfkt7q6z0frcj7vjy4mv7659hccln2e0gzt4ufnsukxcnknh7e58kwnr8pk6kgv6vrghcpld2z5n'; final result = UniversalAddressDetector.detectAddress(invoice); expect(result.isValid, true); diff --git a/test/entities/parse_address_from_domain_test.dart b/test/entities/parse_address_from_domain_test.dart index 0ae9b8d62..b1c3928be 100644 --- a/test/entities/parse_address_from_domain_test.dart +++ b/test/entities/parse_address_from_domain_test.dart @@ -39,8 +39,7 @@ void main() { }); test('extracts Ethereum address correctly', () { - final raw = - 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks'; + final raw = 'Send ETH to 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 thanks'; final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.eth, @@ -60,25 +59,21 @@ void main() { }); test('extracts Bitcoin Cash address correctly', () { - final raw = - 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'; + final raw = 'BCH: bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'; final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.bch, ); - expect( - result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'); + expect(result, 'bitcoincash:qr2z7dusk64qnq97azhg0u0hlf7qgwwfzyj92jgmqj'); }); test('extracts Nano address correctly', () { - final raw = - 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'; + final raw = 'NANO: nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'; final result = AddressResolverUtils.extractAddressByType( raw: raw, type: CryptoCurrency.nano, ); - expect(result, - 'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'); + expect(result, 'nano_1natrium1o3z5519ifou7xii8crpxpk8y65qmkih8e8bpsjri651oza8imdd'); }); test('returns null for unsupported currency', () { @@ -101,9 +96,7 @@ void main() { final raw = 'monero_wallet:467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz?spend_key=029c559cd7669f14e91fd835144916009f8697ab5ac5c7f7c06e1ff869c17b0b&view_key=afaf646edbff3d3bcee8efd3383ffe5d20c947040f74e1110b70ca0fbb0ef90d'; final result = AddressResolverUtils.extractAddressByType( - raw: raw, - type: CryptoCurrency.xmr, - requireSurroundingWhitespaces: false); + raw: raw, type: CryptoCurrency.xmr, requireSurroundingWhitespaces: false); expect(result, '467iotZU5tvG26k2xdZWkJ7gwATFVhfbuV3yDoWx5jHoPwxEi4f5BuJQwkP6GpCb1sZvUVB7nbSkgEuW8NKrh9KKRRga5qz'); }); @@ -119,16 +112,16 @@ sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucw #LTC MWEB ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc '''; - final resultXmr = AddressResolverUtils.extractAddressByType( - raw: raw, type: CryptoCurrency.xmr); + final resultXmr = + AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.xmr); expect(resultXmr, '89bH6i3ftaWSWuPJJYSQuuApWJ8xzinCEbbnAXN1Z3mGGUuAFdpBUg82R9MvJDSheJ6kW2dyMQEFUGM4tsZqRb2Q75UXqvc'); - final resultBtc = AddressResolverUtils.extractAddressByType( - raw: raw, type: CryptoCurrency.btc); + final resultBtc = + AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.btc); expect(resultBtc, 'sp1qq0avpawwjg4l66p6lqafj0vlvm6rlhdc6qt0r6dfual835vhs3gvkq63pechaqezvn7j7uj2jucwj5k7nenpw2r86wf42xv6wqdvxuk5rggrul45'); - final resultLtc = AddressResolverUtils.extractAddressByType( - raw: raw, type: CryptoCurrency.ltc); + final resultLtc = + AddressResolverUtils.extractAddressByType(raw: raw, type: CryptoCurrency.ltc); expect(resultLtc, 'ltcmweb1qq0at62jjucmawxp78qutn0cqwkwahcfx7fxls0r2ma5llg5w6wyy2qe20gxa3rku2658j88zg9d2j4ttpw35k0a5nrg93h5nq3wyvkcgwc3q4dgc'); }); diff --git a/test/utils/payment_request_test.dart b/test/utils/payment_request_test.dart index f7ebbafca..7f219f7f7 100644 --- a/test/utils/payment_request_test.dart +++ b/test/utils/payment_request_test.dart @@ -23,8 +23,7 @@ void main() { }); test("extract address and amount from Cake Style Uri", () { - final uri = - Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00"); + final uri = Uri.parse("ethereum:0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41@1?amount=2.00"); final paymentRequest = PaymentRequest.fromUri(uri); expect(paymentRequest.address, "0xCfc1650da7C961FD82998e7e30ca5f699D0aBf41"); diff --git a/tool/append_translation.dart b/tool/append_translation.dart index 1cc6336b8..0b32eb350 100644 --- a/tool/append_translation.dart +++ b/tool/append_translation.dart @@ -26,11 +26,11 @@ void main(List args) async { } print('Alphabetizing all files...'); - + for (var lang in langs) { final fileName = getArbFileName(lang); alphabetizeArbFile(fileName); } print('Done!'); -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 54febefbb..d6b76d756 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -475,6 +475,7 @@ WalletCredentials createMoneroNewWalletCredentials({required String name, requir Future setLedgerConnection(Object wallet, ledger.LedgerConnection connection); void resetLedgerConnection(); void setGlobalLedgerConnection(ledger.LedgerConnection connection); + bool hasGlobalLedgerConnection(); String? getLastLedgerCommand(); void setHardwareWalletService(Object wallet, HardwareWalletService service); HardwareWalletService getLedgerHardwareWalletService(ledger.LedgerConnection connection); diff --git a/tool/download_moneroc_prebuilds.dart b/tool/download_moneroc_prebuilds.dart index dee046124..5c7373648 100644 --- a/tool/download_moneroc_prebuilds.dart +++ b/tool/download_moneroc_prebuilds.dart @@ -7,7 +7,7 @@ import 'package:archive/archive_io.dart'; final _dio = Dio(); final List triplets = [ - "x86_64-linux-gnu", // linux desktop - majority of users onlinux + "x86_64-linux-gnu", // linux desktop - majority of users onlinux // "i686-linux-gnu", // not supported by cake // "i686-meego-linux-gnu", // sailfishos (emulator)- not supported by cake // "aarch64-linux-gnu", // not (yet) supported by cake - (mostly) mobile linux @@ -18,7 +18,7 @@ final List triplets = [ "armv7a-linux-androideabi", // "i686-w64-mingw32", // 32bit windows - not supported by monero_c "x86_64-w64-mingw32", - // "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake + // "x86_64-apple-darwin11", // Intel macbooks (contrib) - not used by cake // "aarch64-apple-darwin11", // apple silicon macbooks (contrib) - not used by cake // "x86_64-host-apple-darwin", // not available on CI (yet) "aarch64-apple-darwin", // apple silicon macbooks @@ -55,10 +55,8 @@ Future main() async { } if (Platform.isMacOS) { print("Generating ios framework"); - final result = Process.runSync("bash", [ - "-c", - "cd scripts/ios && ./gen_framework.sh && cd ../.." - ]); - print((result.stdout+result.stderr).toString().trim()); + final result = + Process.runSync("bash", ["-c", "cd scripts/ios && ./gen_framework.sh && cd ../.."]); + print((result.stdout + result.stderr).toString().trim()); } -} \ No newline at end of file +} diff --git a/tool/generate_localization.dart b/tool/generate_localization.dart index 4b0927a81..719cf4c5c 100644 --- a/tool/generate_localization.dart +++ b/tool/generate_localization.dart @@ -13,21 +13,21 @@ const srcDir = 'srcDir'; const defaultLocale = 'en'; Future main(List args) async { - final extraInfo = args.isNotEmpty ? - args.fold({}, (Map acc, String arg) { - final parts = arg.split('='); - var key = normalizeKeyName(parts[0]); - if (key.contains('--')) { - key = key.substring(2); - } - acc[key] = parts.length > 1 - ? parts[1].isNotEmpty - ? parts[1] - : inputPath - : inputPath; - return acc; - }) - : {srcDir : inputPath}; + final extraInfo = args.isNotEmpty + ? args.fold({}, (Map acc, String arg) { + final parts = arg.split('='); + var key = normalizeKeyName(parts[0]); + if (key.contains('--')) { + key = key.substring(2); + } + acc[key] = parts.length > 1 + ? parts[1].isNotEmpty + ? parts[1] + : inputPath + : inputPath; + return acc; + }) + : {srcDir: inputPath}; final outputDir = Directory(outputPath); @@ -59,10 +59,10 @@ Future main(List args) async { return; } final parts = - fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_'); + fileName.substring('strings_'.length, fileName.length - '.arb'.length).split('_'); final locale = parts.length > 1 - ? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}' - : parts.first.toLowerCase(); + ? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}' + : parts.first.toLowerCase(); localePath[locale] = element.path; }); @@ -78,8 +78,7 @@ Future main(List args) async { output += part1; output += textDirectionDeclaration; - var inputContent = - File(localePath[defaultLocale].toString()).readAsStringSync(); + var inputContent = File(localePath[defaultLocale].toString()).readAsStringSync(); var config = json.decode(inputContent) as Map; output += localizedStrings(config: config, hasOverride: false); @@ -161,4 +160,3 @@ String localizedStrings({required Map config, required bool has return output; } - diff --git a/tool/generate_new_secrets.dart b/tool/generate_new_secrets.dart index e3695fff6..05e6729f5 100644 --- a/tool/generate_new_secrets.dart +++ b/tool/generate_new_secrets.dart @@ -9,4 +9,4 @@ Future main(List args) async => generateSecrets(args); Future generateSecrets(List args) async { await generateSecretsConfig(args); await importSecretsConfig(); -} \ No newline at end of file +} diff --git a/tool/generate_pubspec.dart b/tool/generate_pubspec.dart index dc2e8131a..14521f36c 100644 --- a/tool/generate_pubspec.dart +++ b/tool/generate_pubspec.dart @@ -9,12 +9,12 @@ Future main(List args) async { final pubspecDescription = File(pubspecDescriptionPath); if (!pubspecBase.existsSync() || !pubspecDescription.existsSync()) { - throw("$pubspecBasePath or $pubspecDescriptionPath doesn't exists"); + throw ("$pubspecBasePath or $pubspecDescriptionPath doesn't exists"); } final pubspecBaseContent = await pubspecBase.readAsString(); final pubspecDescriptionContent = await pubspecDescription.readAsString(); - final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent; + final pubSpecContent = pubspecDescriptionContent + '\n\n' + pubspecBaseContent; final outputPubspec = File(outputPubspecPath); if (outputPubspec.existsSync()) { diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 0b32e60ad..2fecd411b 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -61,9 +61,9 @@ Future generateSecretsConfig(List args) async { return; } } - + await writeConfig(baseConfigFile, SecretKey.base, existingSecrets: secrets); - + await writeConfig(evmChainsConfigFile, SecretKey.evmChainsSecrets); await writeConfig(solanaConfigFile, SecretKey.solanaSecrets); await writeConfig(nanoConfigFile, SecretKey.nanoSecrets); diff --git a/tool/localization/localization_constants.dart b/tool/localization/localization_constants.dart index 68e326823..f6a955a38 100644 --- a/tool/localization/localization_constants.dart +++ b/tool/localization/localization_constants.dart @@ -157,4 +157,4 @@ String getLang(Locale l) => l == null : l.countryCode != null && l.countryCode!.isEmpty ? l.languageCode : l.toString(); -"""; \ No newline at end of file +"""; diff --git a/tool/print_verbose_dummy.dart b/tool/print_verbose_dummy.dart index e69de29bb..8b1378917 100644 --- a/tool/print_verbose_dummy.dart +++ b/tool/print_verbose_dummy.dart @@ -0,0 +1 @@ + diff --git a/tool/translate_changelog.dart b/tool/translate_changelog.dart index 609c969a6..f9f8cde4f 100644 --- a/tool/translate_changelog.dart +++ b/tool/translate_changelog.dart @@ -8,9 +8,9 @@ import 'utils/translation/translation_utils.dart'; const changelogsFilePath = "./assets/new-ui/changelog/text"; void main() async { - final List defaultLangChangelog = jsonDecode( - await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString()) - as List; + final List defaultLangChangelog = + jsonDecode(await File("$changelogsFilePath/changelog_$defaultLang.json").readAsString()) + as List; int currentLang = 0; for (final lang in langs) { @@ -50,7 +50,6 @@ void main() async { "\r$currentLang/${langs.length} langs done, $currentItem/${defaultLangChangelog.length} items done"); } currentLang++; - await File("$changelogsFilePath/changelog_$lang.json") - .writeAsString(jsonEncode(out)); + await File("$changelogsFilePath/changelog_$lang.json").writeAsString(jsonEncode(out)); } } diff --git a/tool/translate_using_llm.dart b/tool/translate_using_llm.dart index e5c22efe1..d5a8d0c6a 100644 --- a/tool/translate_using_llm.dart +++ b/tool/translate_using_llm.dart @@ -217,11 +217,20 @@ IMPORTANT: Respond with a JSON object in the exact format, you must think about void main(List args) async { final parser = ArgParser() - ..addOption('source', abbr: 's', help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)', mandatory: true) - ..addOption('destination', abbr: 'd', help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)', mandatory: true) + ..addOption('source', + abbr: 's', + help: 'Path to source ARB file (e.g., ./res/values/strings_en.arb)', + mandatory: true) + ..addOption('destination', + abbr: 'd', + help: 'Path to destination ARB file (e.g., ./res/values/strings_pl.arb)', + mandatory: true) ..addOption('key', abbr: 'k', help: 'Translate only this specific key (optional)') - ..addOption('ollama-url', help: 'Ollama server URL (default: http://localhost:11434)', defaultsTo: 'http://localhost:11434') - ..addOption('model', help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b') + ..addOption('ollama-url', + help: 'Ollama server URL (default: http://localhost:11434)', + defaultsTo: 'http://localhost:11434') + ..addOption('model', + help: 'Ollama model name (default: gpt-oss:120b)', defaultsTo: 'gpt-oss:120b') ..addFlag('help', abbr: 'h', help: 'Show this help message', negatable: false); try { @@ -268,7 +277,6 @@ void main(List args) async { destinationArbPath: destinationPath, specificKey: specificKey, ); - } catch (e) { print('Error: $e'); print('Run with --help for more information.'); diff --git a/tool/translation_add_lang.dart b/tool/translation_add_lang.dart index 3f509090e..dce93cbf9 100644 --- a/tool/translation_add_lang.dart +++ b/tool/translation_add_lang.dart @@ -8,8 +8,7 @@ import 'utils/translation/translation_utils.dart'; void main(List args) async { if (args.length != 1) { - throw Exception( - 'Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`'); + throw Exception('Insufficient arguments!\n\nTry to run `./translation_add_lang.dart langCode`'); } final targetLang = args.first; diff --git a/tool/translation_consistence.dart b/tool/translation_consistence.dart index f3d3120b0..adba3b418 100644 --- a/tool/translation_consistence.dart +++ b/tool/translation_consistence.dart @@ -28,8 +28,7 @@ void main(List args) async { missingKeys.forEach((key) { print('Missing in "$lang": "$key"'); - if (doFix) - missingDefaults[key] = arbObj[key] as String; + if (doFix) missingDefaults[key] = arbObj[key] as String; }); if (missingDefaults.isNotEmpty) { diff --git a/tool/update_secrets.dart b/tool/update_secrets.dart index 0f2e59419..fd3a58b81 100644 --- a/tool/update_secrets.dart +++ b/tool/update_secrets.dart @@ -12,8 +12,7 @@ Future main(List args) async { } Future updateSecretsConfig(List args) async { - final extraInfo = - args.fold({}, (Map acc, String arg) { + final extraInfo = args.fold({}, (Map acc, String arg) { final parts = arg.split('='); final key = normalizeKeyName(parts[0]); acc[key] = parts.length > 1 ? parts[1] : 1; @@ -32,8 +31,7 @@ Future updateSecretsConfig(List args) async { return false; }); - final fileConfig = - json.decode(configFile.readAsStringSync()) as Map; + final fileConfig = json.decode(configFile.readAsStringSync()) as Map; fileConfig.forEach((key, dynamic value) { if (secrets[key] == null) { secrets[key] = value;