From d1870ba8b87dbe918c0667f588f4376a802a8406 Mon Sep 17 00:00:00 2001 From: Adegoke David <64401859+Blazebrain@users.noreply.github.com> Date: Fri, 3 May 2024 19:00:05 +0100 Subject: [PATCH] CW-525-Add-Tron-Wallet (#1327) * chore: Initial setup for Tron Wallet * feat: Create Tron Wallet base flow implemented, keys, address, receive, restore and proxy classes all setup * feat: Display seed and key within the app * feat: Activate restore from key and seed for Tron wallet * feat: Add icon for tron wallet in wallet listing page * feat: Activate display of receive address for tron * feat: Fetch and display tron balance, sending transaction flow setup, fee limit calculation setup * feat: Implement sending of native tron, setup sending of trc20 tokens * chore: Rename function * Delete lib/tron/tron.dart * feat: Activate exchange for tron and its tokens, implement balance display for trc20 tokens and setup secrets configuration for tron * feat: Implement tron token management, add, remove, delete, and get tokens in home settings view, also minor cleanup * feat: Activate buy and sell for tron * feat: Implement restore from QR, transactions history listing for both native transactions and trc20 transactions * feat: Activate send all and do some minor cleanups * chore: Fix some lint infos and warnings * chore: Adjust configurations * ci: Modify CI to create and add secrets for node * fix: Fixes made while self reviewing the PR for this feature * feat: Add guide for adding new wallet types, and add fixes to requested changes * fix: Handle exceptions gracefully * fix: Alternative for trc20 estimated fee * fix: Fixes to display of amount and fee, removing clashes * fix: Fee calculation WIP * fix: Fix issue with handling of send all flow and display of amount and fee values before broadcasting transaction * fix: PR review fixes and fix merge conflicts * fix: Modify fetching assetOfTransaction [skip ci] * fix: Move tron settings migration to 33 --- .github/workflows/pr_test_build.yml | 2 + .gitignore | 3 + android/app/src/main/AndroidManifestBase.xml | 3 + assets/tron_node_list.yml | 4 + cw_core/lib/crypto_currency.dart | 2 + cw_core/lib/currency_for_wallet_type.dart | 5 +- cw_core/lib/hive_type_ids.dart | 1 + cw_core/lib/node.dart | 2 + cw_core/lib/wallet_type.dart | 16 +- cw_tron/.gitignore | 30 + cw_tron/.metadata | 10 + cw_tron/CHANGELOG.md | 3 + cw_tron/LICENSE | 1 + cw_tron/README.md | 39 ++ cw_tron/analysis_options.yaml | 4 + cw_tron/lib/cw_tron.dart | 7 + cw_tron/lib/default_tron_tokens.dart | 103 ++++ cw_tron/lib/file.dart | 39 ++ cw_tron/lib/pending_tron_transaction.dart | 33 + cw_tron/lib/tron_abi.dart | 436 +++++++++++++ cw_tron/lib/tron_balance.dart | 34 ++ cw_tron/lib/tron_client.dart | 574 ++++++++++++++++++ cw_tron/lib/tron_exception.dart | 16 + cw_tron/lib/tron_http_provider.dart | 41 ++ cw_tron/lib/tron_token.dart | 80 +++ cw_tron/lib/tron_transaction_credentials.dart | 12 + cw_tron/lib/tron_transaction_history.dart | 80 +++ cw_tron/lib/tron_transaction_info.dart | 93 +++ cw_tron/lib/tron_transaction_model.dart | 205 +++++++ cw_tron/lib/tron_wallet.dart | 560 +++++++++++++++++ cw_tron/lib/tron_wallet_addresses.dart | 36 ++ .../lib/tron_wallet_creation_credentials.dart | 29 + cw_tron/lib/tron_wallet_service.dart | 148 +++++ cw_tron/pubspec.yaml | 33 + cw_tron/test/cw_tron_test.dart | 12 + how_to_add_new_wallet_type.md | 300 +++++++++ ios/Runner/InfoBase.plist | 30 + lib/core/address_validator.dart | 2 + lib/core/seed_validator.dart | 3 + lib/di.dart | 3 + lib/entities/default_settings_migration.dart | 41 +- lib/entities/node_list.dart | 20 +- lib/entities/preferences_key.dart | 1 + lib/entities/priority_for_wallet_type.dart | 3 +- lib/entities/provider_types.dart | 13 + lib/main.dart | 2 +- lib/reactions/fiat_rate_update.dart | 6 + lib/reactions/on_current_wallet_change.dart | 14 +- .../desktop_wallet_selection_dropdown.dart | 3 + .../dashboard/pages/transactions_page.dart | 30 +- .../dashboard/widgets/menu_widget.dart | 6 +- .../screens/wallet_list/wallet_list_page.dart | 3 + lib/store/settings_store.dart | 15 + lib/tron/cw_tron.dart | 114 ++++ .../advanced_privacy_settings_view_model.dart | 1 + .../dashboard/balance_view_model.dart | 7 +- .../dashboard/home_settings_view_model.dart | 33 +- .../dashboard/transaction_list_item.dart | 46 ++ .../exchange/exchange_trade_view_model.dart | 7 +- .../exchange/exchange_view_model.dart | 4 + .../node_create_or_edit_view_model.dart | 1 + .../node_list/node_list_view_model.dart | 3 + .../restore/restore_from_qr_vm.dart | 9 +- .../restore/wallet_restore_from_qr_code.dart | 11 + lib/view_model/send/output.dart | 22 +- .../send/send_template_view_model.dart | 3 +- lib/view_model/send/send_view_model.dart | 19 +- .../settings/other_settings_view_model.dart | 5 +- .../transaction_details_view_model.dart | 54 +- .../wallet_address_list_view_model.dart | 26 + lib/view_model/wallet_keys_view_model.dart | 5 +- lib/view_model/wallet_new_vm.dart | 4 + lib/view_model/wallet_restore_view_model.dart | 17 +- model_generator.sh | 1 + pubspec_base.yaml | 1 + scripts/android/pubspec_gen.sh | 2 +- scripts/ios/app_config.sh | 2 +- scripts/macos/app_config.sh | 2 +- tool/configure.dart | 96 ++- tool/generate_secrets_config.dart | 18 +- tool/import_secrets_config.dart | 14 + tool/utils/secret_key.dart | 4 + 82 files changed, 3660 insertions(+), 62 deletions(-) create mode 100644 assets/tron_node_list.yml create mode 100644 cw_tron/.gitignore create mode 100644 cw_tron/.metadata create mode 100644 cw_tron/CHANGELOG.md create mode 100644 cw_tron/LICENSE create mode 100644 cw_tron/README.md create mode 100644 cw_tron/analysis_options.yaml create mode 100644 cw_tron/lib/cw_tron.dart create mode 100644 cw_tron/lib/default_tron_tokens.dart create mode 100644 cw_tron/lib/file.dart create mode 100644 cw_tron/lib/pending_tron_transaction.dart create mode 100644 cw_tron/lib/tron_abi.dart create mode 100644 cw_tron/lib/tron_balance.dart create mode 100644 cw_tron/lib/tron_client.dart create mode 100644 cw_tron/lib/tron_exception.dart create mode 100644 cw_tron/lib/tron_http_provider.dart create mode 100644 cw_tron/lib/tron_token.dart create mode 100644 cw_tron/lib/tron_transaction_credentials.dart create mode 100644 cw_tron/lib/tron_transaction_history.dart create mode 100644 cw_tron/lib/tron_transaction_info.dart create mode 100644 cw_tron/lib/tron_transaction_model.dart create mode 100644 cw_tron/lib/tron_wallet.dart create mode 100644 cw_tron/lib/tron_wallet_addresses.dart create mode 100644 cw_tron/lib/tron_wallet_creation_credentials.dart create mode 100644 cw_tron/lib/tron_wallet_service.dart create mode 100644 cw_tron/pubspec.yaml create mode 100644 cw_tron/test/cw_tron_test.dart create mode 100644 how_to_add_new_wallet_type.md create mode 100644 lib/tron/cw_tron.dart diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index dc231df42..46924cb35 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -113,6 +113,7 @@ jobs: touch lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart touch cw_solana/lib/.secrets.g.dart + touch cw_tron/lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart @@ -150,6 +151,7 @@ jobs: echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart echo "const ankrApiKey = '${{ secrets.ANKR_API_KEY }}';" >> cw_solana/lib/.secrets.g.dart + echo "const tronGridApiKey = '${{ secrets.TRON_GRID_API_KEY }}';" >> cw_tron/lib/.secrets.g.dart - name: Rename app run: | diff --git a/.gitignore b/.gitignore index 6f2d0a182..f1e5b6da3 100644 --- a/.gitignore +++ b/.gitignore @@ -94,9 +94,11 @@ android/app/key.jks **/tool/.evm-secrets-config.json **/tool/.ethereum-secrets-config.json **/tool/.solana-secrets-config.json +**/tool/.tron-secrets-config.json **/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart +**/cw_tron/lib/.secrets.g.dart vendor/ @@ -132,6 +134,7 @@ lib/bitcoin_cash/bitcoin_cash.dart lib/nano/nano.dart lib/polygon/polygon.dart lib/solana/solana.dart +lib/tron/tron.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index eea9b5521..485f049e8 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -67,6 +67,9 @@ + + + with Serializable implemen CryptoCurrency.kaspa, CryptoCurrency.digibyte, CryptoCurrency.usdtSol, + CryptoCurrency.usdcTrc20, ]; static const havenCurrencies = [ @@ -217,6 +218,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static const digibyte = CryptoCurrency(title: 'DGB', fullName: 'DigiByte', raw: 90, name: 'dgb', iconPath: 'assets/images/digibyte.png', decimals: 8); static const usdtSol = CryptoCurrency(title: 'USDT', tag: 'SOL', fullName: 'USDT Tether', raw: 91, name: 'usdtsol', iconPath: 'assets/images/usdt_icon.png', decimals: 6); + static const usdcTrc20 = CryptoCurrency(title: 'USDC', tag: 'TRX', fullName: 'USDC Coin', raw: 92, name: 'usdctrc20', iconPath: 'assets/images/usdc_icon.png', decimals: 6); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 58ee37669..92e78b2e6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -23,7 +23,10 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.maticpoly; case WalletType.solana: return CryptoCurrency.sol; + case WalletType.tron: + return CryptoCurrency.trx; default: - throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); + throw Exception( + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index e0896bab1..e3332a043 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -16,3 +16,4 @@ const POW_NODE_TYPE_ID = 14; const DERIVATION_TYPE_TYPE_ID = 15; const SPL_TOKEN_TYPE_ID = 16; const DERIVATION_INFO_TYPE_ID = 17; +const TRON_TOKEN_TYPE_ID = 18; diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 9d0806851..1195b6819 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -94,6 +94,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return Uri.https(uriRaw, path ?? ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); @@ -152,6 +153,7 @@ class Node extends HiveObject with Keyable { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return requestElectrumServer(); default: return false; diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index a63ddf37c..e846093d0 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -15,6 +15,7 @@ const walletTypes = [ WalletType.banano, WalletType.polygon, WalletType.solana, + WalletType.tron, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -50,7 +51,10 @@ enum WalletType { polygon, @HiveField(10) - solana + solana, + + @HiveField(11) + tron } int serializeToInt(WalletType type) { @@ -75,6 +79,8 @@ int serializeToInt(WalletType type) { return 8; case WalletType.solana: return 9; + case WalletType.tron: + return 10; default: return -1; } @@ -102,6 +108,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.polygon; case 9: return WalletType.solana; + case 10: + return WalletType.tron; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -129,6 +137,8 @@ String walletTypeToString(WalletType type) { return 'Polygon'; case WalletType.solana: return 'Solana'; + case WalletType.tron: + return 'Tron'; default: return ''; } @@ -156,6 +166,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Polygon (MATIC)'; case WalletType.solana: return 'Solana (SOL)'; + case WalletType.tron: + return 'Tron (TRX)'; default: return ''; } @@ -183,6 +195,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.maticpoly; case WalletType.solana: return CryptoCurrency.sol; + case WalletType.tron: + return CryptoCurrency.trx; default: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_tron/.gitignore b/cw_tron/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_tron/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/cw_tron/.metadata b/cw_tron/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_tron/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_tron/CHANGELOG.md b/cw_tron/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_tron/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_tron/LICENSE b/cw_tron/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_tron/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_tron/README.md b/cw_tron/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_tron/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_tron/analysis_options.yaml b/cw_tron/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_tron/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_tron/lib/cw_tron.dart b/cw_tron/lib/cw_tron.dart new file mode 100644 index 000000000..6981fccba --- /dev/null +++ b/cw_tron/lib/cw_tron.dart @@ -0,0 +1,7 @@ +library cw_tron; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_tron/lib/default_tron_tokens.dart b/cw_tron/lib/default_tron_tokens.dart new file mode 100644 index 000000000..ad70f28cd --- /dev/null +++ b/cw_tron/lib/default_tron_tokens.dart @@ -0,0 +1,103 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_tron/tron_token.dart'; + +class DefaultTronTokens { + final List _defaultTokens = [ + TronToken( + name: "Tether USD", + symbol: "USDT", + contractAddress: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimal: 6, + enabled: true, + ), + TronToken( + name: "USD Coin", + symbol: "USDC", + contractAddress: "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", + decimal: 6, + enabled: true, + ), + TronToken( + name: "Bitcoin", + symbol: "BTC", + contractAddress: "TN3W4H6rK2ce4vX9YnFQHwKENnHjoxb3m9", + decimal: 8, + enabled: true, + ), + TronToken( + name: "Ethereum", + symbol: "ETH", + contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh", + decimal: 18, + enabled: true, + ), + TronToken( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "TXpw8XeWYeTUd4quDskoUqeQPowRh4jY65", + decimal: 8, + enabled: true, + ), + TronToken( + name: "Dogecoin", + symbol: "DOGE", + contractAddress: "THbVQp8kMjStKNnf2iCY6NEzThKMK5aBHg", + decimal: 8, + enabled: true, + ), + TronToken( + name: "JUST Stablecoin", + symbol: "USDJ", + contractAddress: "TMwFHYXLJaRUPeW6421aqXL4ZEzPRFGkGT", + decimal: 18, + enabled: false, + ), + TronToken( + name: "SUN", + symbol: "SUN", + contractAddress: "TSSMHYeV2uE9qYH95DqyoCuNCzEL1NvU3S", + decimal: 18, + enabled: false, + ), + TronToken( + name: "Wrapped TRX", + symbol: "WTRX", + contractAddress: "TNUC9Qb1rRpS5CbWLmNMxXBjyFoydXjWFR", + decimal: 6, + enabled: false, + ), + TronToken( + name: "BitTorent", + symbol: "BTT", + contractAddress: "TAFjULxiVgT4qWk6UZwjqwZXTSaGaqnVp4", + decimal: 18, + enabled: false, + ), + TronToken( + name: "BUSD Token", + symbol: "BUSD", + contractAddress: "TMz2SWatiAtZVVcH2ebpsbVtYwUPT9EdjH", + decimal: 18, + enabled: false, + ), + TronToken( + name: "HTX", + symbol: "HTX", + contractAddress: "TUPM7K8REVzD2UdV4R5fe5M8XbnR2DdoJ6", + decimal: 18, + enabled: false, + ), + ]; + + List get initialTronTokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => + element.title.toUpperCase() == token.symbol.split(".").first.toUpperCase()) + .iconPath; + } catch (_) {} + + return TronToken.copyWith(token, iconPath, 'TRX'); + }).toList(); +} diff --git a/cw_tron/lib/file.dart b/cw_tron/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_tron/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_tron/lib/pending_tron_transaction.dart b/cw_tron/lib/pending_tron_transaction.dart new file mode 100644 index 000000000..b6d064b31 --- /dev/null +++ b/cw_tron/lib/pending_tron_transaction.dart @@ -0,0 +1,33 @@ + + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingTronTransaction with PendingTransaction { + final Function sendTransaction; + final List signedTransaction; + final String fee; + final String amount; + + PendingTronTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + }); + + @override + String get amountFormatted => amount; + + @override + Future commit() async => await sendTransaction(); + + @override + String get feeFormatted => fee; + + @override + String get hex => bytesToHex(signedTransaction); + + @override + String get id => ''; +} diff --git a/cw_tron/lib/tron_abi.dart b/cw_tron/lib/tron_abi.dart new file mode 100644 index 000000000..fdb998636 --- /dev/null +++ b/cw_tron/lib/tron_abi.dart @@ -0,0 +1,436 @@ +final trc20Abi = [ + {"inputs": [], "stateMutability": "nonpayable", "type": "constructor"}, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "owner", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "spender", "type": "address"}, + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "uint256", "name": "total", "type": "uint256"}, + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "contract_address", "type": "address"} + ], + "name": "OrderPaid", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "previousOwner", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "newOwner", "type": "address"} + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "address", "name": "token", "type": "address"}, + {"indexed": false, "internalType": "bool", "name": "active", "type": "bool"} + ], + "name": "TokenUpdate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "address", "name": "from", "type": "address"}, + {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, + {"indexed": false, "internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": false, "internalType": "string", "name": "username", "type": "string"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"} + ], + "name": "UserRegistred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "buyer", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "seller", "type": "address"} + ], + "name": "WBuyer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + {"indexed": true, "internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"indexed": true, "internalType": "address", "name": "seller", "type": "address"}, + {"indexed": false, "internalType": "address", "name": "buyer", "type": "address"} + ], + "name": "WSeller", + "type": "event" + }, + { + "inputs": [], + "name": "CONTRACTPERCENTAGE", + "outputs": [ + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "uint256", "name": "order_total", "type": "uint256"}, + {"internalType": "address", "name": "contractAddress", "type": "address"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "PayWithTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "TOKENINCREAMENT", + "outputs": [ + {"internalType": "uint16", "name": "", "type": "uint16"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_signer", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_tokens", + "outputs": [ + {"internalType": "bool", "name": "active", "type": "bool"}, + {"internalType": "uint16", "name": "token", "type": "uint16"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "name": "_users", + "outputs": [ + {"internalType": "bool", "name": "active", "type": "bool"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "owner", "type": "address"}, + {"internalType": "address", "name": "spender", "type": "address"} + ], + "name": "allowance", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "approve", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"} + ], + "name": "balanceOf", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"} + ], + "name": "balanceOfContract", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "account", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "burnFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint256", "name": "value", "type": "uint256"}, + {"internalType": "address", "name": "_contractAddress", "type": "address"} + ], + "name": "contractWithdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "subtractedValue", "type": "uint256"} + ], + "name": "decreaseAllowance", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "spender", "type": "address"}, + {"internalType": "uint256", "name": "addedValue", "type": "uint256"} + ], + "name": "increaseAllowance", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + {"internalType": "address", "name": "", "type": "address"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "token", "type": "address"}, + {"internalType": "uint256", "name": "value", "type": "uint256"} + ], + "name": "payToContract", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "payWithNativeToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "string", "name": "username", "type": "string"} + ], + "name": "regiserUser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint16", "name": "id", "type": "uint16"}, + {"internalType": "address", "name": "buyer", "type": "address"}, + {"internalType": "address", "name": "seller", "type": "address"} + ], + "name": "selectOrder", + "outputs": [ + {"internalType": "uint232", "name": "", "type": "uint232"}, + {"internalType": "uint16", "name": "", "type": "uint16"}, + {"internalType": "uint8", "name": "", "type": "uint8"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + {"internalType": "string", "name": "", "type": "string"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "signer", "type": "address"} + ], + "name": "toggleSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "tokenAddress", "type": "address"} + ], + "name": "toggleToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + {"internalType": "uint256", "name": "", "type": "uint256"} + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transfer", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "from", "type": "address"}, + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"} + ], + "name": "transferFrom", + "outputs": [ + {"internalType": "bool", "name": "", "type": "bool"} + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "newOwner", "type": "address"} + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "uint8", "name": "newPercentage", "type": "uint8"} + ], + "name": "updateContractPercentage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address[]", "name": "buyer", "type": "address[]"}, + {"internalType": "bytes[]", "name": "signature", "type": "bytes[]"}, + {"internalType": "uint16[]", "name": "order_id", "type": "uint16[]"}, + {"internalType": "address", "name": "contractAddress", "type": "address"} + ], + "name": "widthrawForSellers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + {"internalType": "address", "name": "seller", "type": "address"}, + {"internalType": "bytes", "name": "signature", "type": "bytes"}, + {"internalType": "uint16", "name": "order_id", "type": "uint16"}, + {"internalType": "address", "name": "contractAddress", "type": "address"} + ], + "name": "widthrowForBuyers", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +]; diff --git a/cw_tron/lib/tron_balance.dart b/cw_tron/lib/tron_balance.dart new file mode 100644 index 000000000..5b2ba3fa7 --- /dev/null +++ b/cw_tron/lib/tron_balance.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:cw_core/balance.dart'; +import 'package:on_chain/on_chain.dart'; + +class TronBalance extends Balance { + TronBalance(this.balance) : super(balance.toInt(), balance.toInt()); + + final BigInt balance; + + @override + String get formattedAdditionalBalance => TronHelper.fromSun(balance); + + @override + String get formattedAvailableBalance => TronHelper.fromSun(balance); + + String toJSON() => json.encode({ + 'balance': balance.toString(), + }); + + static TronBalance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return TronBalance(BigInt.parse(decoded['balance'])); + } catch (e) { + return TronBalance(BigInt.zero); + } + } +} diff --git a/cw_tron/lib/tron_client.dart b/cw_tron/lib/tron_client.dart new file mode 100644 index 000000000..f03a8abce --- /dev/null +++ b/cw_tron/lib/tron_client.dart @@ -0,0 +1,574 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; + +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_tron/pending_tron_transaction.dart'; +import 'package:cw_tron/tron_abi.dart'; +import 'package:cw_tron/tron_balance.dart'; +import 'package:cw_tron/tron_http_provider.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_transaction_model.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import '.secrets.g.dart' as secrets; +import 'package:on_chain/on_chain.dart'; + +class TronClient { + final httpClient = Client(); + TronProvider? _provider; + // This is an internal tracker, so we don't have to "refetch". + int _nativeTxEstimatedFee = 0; + + int get chainId => 1000; + + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get( + Uri.https( + "api.trongrid.io", + "/v1/accounts/$address/transactions", + { + "only_confirmed": "true", + "limit": "200", + }, + ), + headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + ); + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && + response.statusCode < 300 && + jsonResponse['status'] != false) { + return (jsonResponse['data'] as List).map((e) { + return TronTransactionModel.fromJson(e as Map); + }).toList(); + } + + return []; + } catch (e, s) { + log('Error getting tx: ${e.toString()}\n ${s.toString()}'); + return []; + } + } + + Future> fetchTrc20ExcludedTransactions(String address) async { + try { + final response = await httpClient.get( + Uri.https( + "api.trongrid.io", + "/v1/accounts/$address/transactions/trc20", + { + "only_confirmed": "true", + "limit": "200", + }, + ), + headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + ); + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && + response.statusCode < 300 && + jsonResponse['status'] != false) { + return (jsonResponse['data'] as List).map((e) { + return TronTRC20TransactionModel.fromJson(e as Map); + }).toList(); + } + + return []; + } catch (e, s) { + log('Error getting trc20 tx: ${e.toString()}\n ${s.toString()}'); + return []; + } + } + + bool connect(Node node) { + try { + final formattedUrl = '${node.isSSL ? 'https' : 'http'}://${node.uriRaw}'; + _provider = TronProvider(TronHTTPProvider(url: formattedUrl)); + + return true; + } catch (e) { + return false; + } + } + + Future getBalance(TronAddress address) async { + try { + final accountDetails = await _provider!.request(TronRequestGetAccount(address: address)); + + return accountDetails?.balance ?? BigInt.zero; + } catch (_) { + return BigInt.zero; + } + } + + Future getFeeLimit( + TransactionRaw rawTransaction, + TronAddress address, + TronAddress receiverAddress, { + int energyUsed = 0, + bool isEstimatedFeeFlow = false, + }) async { + try { + // Get the tron chain parameters. + final chainParams = await _provider!.request(TronRequestGetChainParameters()); + + final bandWidthInSun = chainParams.getTransactionFee!; + log('BandWidth In Sun: $bandWidthInSun'); + + final energyInSun = chainParams.getEnergyFee!; + log('Energy In Sun: $energyInSun'); + + log( + 'Create Account Fee In System Contract for Chain: ${chainParams.getCreateNewAccountFeeInSystemContract!}', + ); + log('Create Account Fee for Chain: ${chainParams.getCreateAccountFee}'); + + final fakeTransaction = Transaction( + rawData: rawTransaction, + signature: [Uint8List(65)], + ); + + // Calculate the total size of the fake transaction, considering the required network overhead. + final transactionSize = fakeTransaction.length + 64; + + // Assign the calculated size to the variable representing the required bandwidth. + int neededBandWidth = transactionSize; + log('Initial Needed Bandwidth: $neededBandWidth'); + + int neededEnergy = energyUsed; + log('Initial Needed Energy: $neededEnergy'); + + // Fetch account resources to assess the available bandwidth and energy + final accountResource = + await _provider!.request(TronRequestGetAccountResource(address: address)); + + neededEnergy -= accountResource.howManyEnergy.toInt(); + log('Account resource energy: ${accountResource.howManyEnergy.toInt()}'); + log('Needed Energy after deducting from account resource energy: $neededEnergy'); + + // Deduct the bandwidth from the account's available bandwidth. + final BigInt accountBandWidth = accountResource.howManyBandwIth; + log('Account resource bandwidth: ${accountResource.howManyBandwIth.toInt()}'); + + if (accountBandWidth >= BigInt.from(neededBandWidth) && !isEstimatedFeeFlow) { + log('Account has more bandwidth than required'); + neededBandWidth = 0; + } + + if (neededEnergy < 0) { + neededEnergy = 0; + } + + final energyBurn = neededEnergy * energyInSun.toInt(); + log('Energy Burn: $energyBurn'); + + final bandWidthBurn = neededBandWidth * bandWidthInSun; + log('Bandwidth Burn: $bandWidthBurn'); + + int totalBurn = energyBurn + bandWidthBurn; + log('Total Burn: $totalBurn'); + + /// If there is a note (memo), calculate the memo fee. + if (rawTransaction.data != null) { + totalBurn += chainParams.getMemoFee!; + } + + // Check if receiver's account is active + final receiverAccountInfo = + await _provider!.request(TronRequestGetAccount(address: receiverAddress)); + + /// Calculate the resources required to create a new account. + if (receiverAccountInfo == null) { + totalBurn += chainParams.getCreateNewAccountFeeInSystemContract!; + + totalBurn += (chainParams.getCreateAccountFee! * bandWidthInSun); + } + + log('Final total burn: $totalBurn'); + + return totalBurn; + } catch (_) { + return 0; + } + } + + Future getEstimatedFee(TronAddress ownerAddress) async { + const constantAmount = '1000'; + // Fetch the latest Tron block + final block = await _provider!.request(TronRequestGetNowBlock()); + + // Create the transfer contract + final contract = TransferContract( + amount: TronHelper.toSun(constantAmount), + ownerAddress: ownerAddress, + toAddress: ownerAddress, + ); + + // Prepare the contract parameter for the transaction. + final parameter = Any(typeUrl: contract.typeURL, value: contract); + + // Create a TransactionContract object with the contract type and parameter. + final transactionContract = + TransactionContract(type: contract.contractType, parameter: parameter); + + // Set the transaction expiration time (maximum 24 hours) + final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24)); + + // Create a raw transaction + TransactionRaw rawTransaction = TransactionRaw( + refBlockBytes: block.blockHeader.rawData.refBlockBytes, + refBlockHash: block.blockHeader.rawData.refBlockHash, + expiration: BigInt.from(expireTime.millisecondsSinceEpoch), + contract: [transactionContract], + timestamp: block.blockHeader.rawData.timestamp, + ); + + final estimatedFee = await getFeeLimit( + rawTransaction, + ownerAddress, + ownerAddress, + isEstimatedFeeFlow: true, + ); + + _nativeTxEstimatedFee = estimatedFee; + + return estimatedFee; + } + + Future getTRCEstimatedFee(TronAddress ownerAddress) async { + String contractAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; + String constantAmount = + '0'; // We're using 0 as the base amount here as we get an error when balance is zero i.e for new wallets. + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("transfer"); + + /// address /// amount + final transferparams = [ + ownerAddress, + TronHelper.toSun(constantAmount), + ]; + + final contractAddr = TronAddress(contractAddress); + + final request = await _provider!.request( + TronRequestTriggerConstantContract( + ownerAddress: ownerAddress, + contractAddress: contractAddr, + data: function.encodeHex(transferparams), + ), + ); + + if (!request.isSuccess) { + log("Tron TRC20 error: ${request.error} \n ${request.respose}"); + } + + final feeLimit = await getFeeLimit( + request.transactionRaw!, + ownerAddress, + ownerAddress, + energyUsed: request.energyUsed ?? 0, + isEstimatedFeeFlow: true, + ); + return feeLimit; + } + + Future signTransaction({ + required TronPrivateKey ownerPrivKey, + required String toAddress, + required String amount, + required CryptoCurrency currency, + required BigInt tronBalance, + required bool sendAll, + }) async { + // Get the owner tron address from the key + final ownerAddress = ownerPrivKey.publicKey().toAddress(); + + // Define the receiving Tron address for the transaction. + final receiverAddress = TronAddress(toAddress); + + bool isNativeTransaction = currency == CryptoCurrency.trx; + + String totalAmount; + TransactionRaw rawTransaction; + if (isNativeTransaction) { + if (sendAll) { + final accountResource = + await _provider!.request(TronRequestGetAccountResource(address: ownerAddress)); + + final availableBandWidth = accountResource.howManyBandwIth.toInt(); + + // 269 is the current middle ground for bandwidth per transaction + if (availableBandWidth >= 269) { + totalAmount = amount; + } else { + final amountInSun = TronHelper.toSun(amount).toInt(); + + // 5000 added here is a buffer since we're working with "estimated" value of the fee. + final result = amountInSun - (_nativeTxEstimatedFee + 5000); + + totalAmount = TronHelper.fromSun(BigInt.from(result)); + } + } else { + totalAmount = amount; + } + rawTransaction = await _signNativeTransaction( + ownerAddress, + receiverAddress, + totalAmount, + tronBalance, + sendAll, + ); + } else { + final tokenAddress = (currency as TronToken).contractAddress; + totalAmount = amount; + rawTransaction = await _signTrcTokenTransaction( + ownerAddress, + receiverAddress, + totalAmount, + tokenAddress, + tronBalance, + ); + } + + final signature = ownerPrivKey.sign(rawTransaction.toBuffer()); + + sendTx() async => await sendTransaction( + rawTransaction: rawTransaction, + signature: signature, + ); + + return PendingTronTransaction( + signedTransaction: signature, + amount: totalAmount, + fee: TronHelper.fromSun(rawTransaction.feeLimit ?? BigInt.zero), + sendTransaction: sendTx, + ); + } + + Future _signNativeTransaction( + TronAddress ownerAddress, + TronAddress receiverAddress, + String amount, + BigInt tronBalance, + bool sendAll, + ) async { + // This is introduce to server as a limit in cases where feeLimit is 0 + // The transaction signing will fail if the feeLimit is explicitly 0. + int defaultFeeLimit = 100000; + + final block = await _provider!.request(TronRequestGetNowBlock()); + // Create the transfer contract + final contract = TransferContract( + amount: TronHelper.toSun(amount), + ownerAddress: ownerAddress, + toAddress: receiverAddress, + ); + + // Prepare the contract parameter for the transaction. + final parameter = Any(typeUrl: contract.typeURL, value: contract); + + // Create a TransactionContract object with the contract type and parameter. + final transactionContract = + TransactionContract(type: contract.contractType, parameter: parameter); + + // Set the transaction expiration time (maximum 24 hours) + final expireTime = DateTime.now().toUtc().add(const Duration(hours: 24)); + + // Create a raw transaction + TransactionRaw rawTransaction = TransactionRaw( + refBlockBytes: block.blockHeader.rawData.refBlockBytes, + refBlockHash: block.blockHeader.rawData.refBlockHash, + expiration: BigInt.from(expireTime.millisecondsSinceEpoch), + contract: [transactionContract], + timestamp: block.blockHeader.rawData.timestamp, + ); + + final feeLimit = await getFeeLimit(rawTransaction, ownerAddress, receiverAddress); + final feeLimitToUse = feeLimit != 0 ? feeLimit : defaultFeeLimit; + final tronBalanceInt = tronBalance.toInt(); + + if (feeLimit > tronBalanceInt) { + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.', + ); + } + + rawTransaction = rawTransaction.copyWith( + feeLimit: BigInt.from(feeLimitToUse), + ); + + return rawTransaction; + } + + Future _signTrcTokenTransaction( + TronAddress ownerAddress, + TronAddress receiverAddress, + String amount, + String contractAddress, + BigInt tronBalance, + ) async { + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("transfer"); + + /// address /// amount + final transferparams = [ + receiverAddress, + TronHelper.toSun(amount), + ]; + + final contractAddr = TronAddress(contractAddress); + + final request = await _provider!.request( + TronRequestTriggerConstantContract( + ownerAddress: ownerAddress, + contractAddress: contractAddr, + data: function.encodeHex(transferparams), + ), + ); + + if (!request.isSuccess) { + log("Tron TRC20 error: ${request.error} \n ${request.respose}"); + } + + final feeLimit = await getFeeLimit( + request.transactionRaw!, + ownerAddress, + receiverAddress, + energyUsed: request.energyUsed ?? 0, + ); + + final tronBalanceInt = tronBalance.toInt(); + + if (feeLimit > tronBalanceInt) { + throw Exception( + 'You don\'t have enough TRX to cover the transaction fee for this transaction. Kindly top up.', + ); + } + + final rawTransaction = request.transactionRaw!.copyWith( + feeLimit: BigInt.from(feeLimit), + ); + + return rawTransaction; + } + + Future sendTransaction({ + required TransactionRaw rawTransaction, + required List signature, + }) async { + try { + final transaction = Transaction(rawData: rawTransaction, signature: [signature]); + + final raw = BytesUtils.toHexString(transaction.toBuffer()); + + final txBroadcastResult = await _provider!.request(TronRequestBroadcastHex(transaction: raw)); + + if (txBroadcastResult.isSuccess) { + return txBroadcastResult.txId!; + } else { + throw Exception(txBroadcastResult.error); + } + } catch (e) { + log('Send block Exception: ${e.toString()}'); + throw Exception(e); + } + } + + Future fetchTronTokenBalances(String userAddress, String contractAddress) async { + try { + final ownerAddress = TronAddress(userAddress); + + final tokenAddress = TronAddress(contractAddress); + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final function = contract.functionFromName("balanceOf"); + + final request = await _provider!.request( + TronRequestTriggerConstantContract.fromMethod( + ownerAddress: ownerAddress, + contractAddress: tokenAddress, + function: function, + params: [ownerAddress], + ), + ); + + final outputResult = request.outputResult?.first ?? BigInt.zero; + + return TronBalance(outputResult); + } catch (_) { + return TronBalance(BigInt.zero); + } + } + + Future getTronToken(String contractAddress, String userAddress) async { + try { + final tokenAddress = TronAddress(contractAddress); + + final ownerAddress = TronAddress(userAddress); + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final name = + (await getTokenDetail(contract, "name", ownerAddress, tokenAddress) as String?) ?? ''; + + final symbol = + (await getTokenDetail(contract, "symbol", ownerAddress, tokenAddress) as String?) ?? ''; + + final decimal = + (await getTokenDetail(contract, "decimals", ownerAddress, tokenAddress) as BigInt?) ?? + BigInt.zero; + + return TronToken( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + Future getTokenDetail( + ContractABI contract, + String functionName, + TronAddress ownerAddress, + TronAddress tokenAddress, + ) async { + final function = contract.functionFromName(functionName); + + try { + final request = await _provider!.request( + TronRequestTriggerConstantContract.fromMethod( + ownerAddress: ownerAddress, + contractAddress: tokenAddress, + function: function, + params: [], + ), + ); + + final outputResult = request.outputResult?.first; + + return outputResult; + } catch (_) { + log('Erorr fetching detail: ${_.toString()}'); + + return null; + } + } +} diff --git a/cw_tron/lib/tron_exception.dart b/cw_tron/lib/tron_exception.dart new file mode 100644 index 000000000..13b98c024 --- /dev/null +++ b/cw_tron/lib/tron_exception.dart @@ -0,0 +1,16 @@ +import 'package:cw_core/crypto_currency.dart'; + +class TronMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Tron mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} +class TronTransactionCreationException implements Exception { + final String exceptionMessage; + + TronTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} \ No newline at end of file diff --git a/cw_tron/lib/tron_http_provider.dart b/cw_tron/lib/tron_http_provider.dart new file mode 100644 index 000000000..193a3dbdd --- /dev/null +++ b/cw_tron/lib/tron_http_provider.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:on_chain/tron/tron.dart'; +import '.secrets.g.dart' as secrets; + +class TronHTTPProvider implements TronServiceProvider { + TronHTTPProvider( + {required this.url, + http.Client? client, + this.defaultRequestTimeout = const Duration(seconds: 30)}) + : client = client ?? http.Client(); + @override + final String url; + final http.Client client; + final Duration defaultRequestTimeout; + + @override + Future> get(TronRequestDetails params, [Duration? timeout]) async { + final response = await client.get(Uri.parse(params.url(url)), headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }).timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } + + @override + Future> post(TronRequestDetails params, [Duration? timeout]) async { + final response = await client + .post(Uri.parse(params.url(url)), + headers: { + 'Content-Type': 'application/json', + 'TRON-PRO-API-KEY': secrets.tronGridApiKey, + }, + body: params.toRequestBody()) + .timeout(timeout ?? defaultRequestTimeout); + final data = json.decode(response.body) as Map; + return data; + } +} diff --git a/cw_tron/lib/tron_token.dart b/cw_tron/lib/tron_token.dart new file mode 100644 index 000000000..8c45ab486 --- /dev/null +++ b/cw_tron/lib/tron_token.dart @@ -0,0 +1,80 @@ +// ignore_for_file: annotate_overrides, overridden_fields + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'tron_token.g.dart'; + +@HiveType(typeId: TronToken.typeId) +class TronToken extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + + @HiveField(1) + final String symbol; + + @HiveField(2) + final String contractAddress; + + @HiveField(3) + final int decimal; + + @HiveField(4, defaultValue: true) + bool _enabled; + + @HiveField(5) + final String? iconPath; + + @HiveField(6) + final String? tag; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + TronToken({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + this.tag = 'TRX', + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: tag, + iconPath: iconPath, + decimals: decimal); + + TronToken.copyWith(TronToken other, String? icon, String? tag) + : name = other.name, + symbol = other.symbol, + contractAddress = other.contractAddress, + decimal = other.decimal, + _enabled = other.enabled, + tag = tag ?? other.tag, + iconPath = icon ?? other.iconPath, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: tag ?? other.tag, + iconPath: icon ?? other.iconPath, + decimals: other.decimal, + ); + + static const typeId = TRON_TOKEN_TYPE_ID; + static const boxName = 'TronTokens'; + + @override + bool operator ==(other) => + (other is TronToken && other.contractAddress == contractAddress) || + (other is CryptoCurrency && other.title == title); + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_tron/lib/tron_transaction_credentials.dart b/cw_tron/lib/tron_transaction_credentials.dart new file mode 100644 index 000000000..e68d5525b --- /dev/null +++ b/cw_tron/lib/tron_transaction_credentials.dart @@ -0,0 +1,12 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; + +class TronTransactionCredentials { + TronTransactionCredentials( + this.outputs, { + required this.currency, + }); + + final List outputs; + final CryptoCurrency currency; +} diff --git a/cw_tron/lib/tron_transaction_history.dart b/cw_tron/lib/tron_transaction_history.dart new file mode 100644 index 000000000..7d7274226 --- /dev/null +++ b/cw_tron/lib/tron_transaction_history.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_evm/file.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'tron_transaction_history.g.dart'; + +class TronTransactionHistory = TronTransactionHistoryBase with _$TronTransactionHistory; + +abstract class TronTransactionHistoryBase extends TransactionHistoryBase + with Store { + TronTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + String _password; + + final WalletInfo walletInfo; + + Future init() async => await _load(); + + @override + Future save() async { + String transactionsHistoryFileNameForWallet = 'tron_transactions.json'; + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final transactionMaps = transactions.map((key, value) => MapEntry(key, value.toJson())); + final data = json.encode({'transactions': transactionMaps}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); + log(s.toString()); + } + } + + @override + void addOne(TronTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + String transactionsHistoryFileNameForWallet = 'tron_transactions.json'; + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + for (var entry in txs.entries) { + final val = entry.value; + + if (val is Map) { + final tx = TronTransactionInfo.fromJson(val); + _update(tx); + } + } + } catch (e) { + log(e.toString()); + } + } + + void _update(TronTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_tron/lib/tron_transaction_info.dart b/cw_tron/lib/tron_transaction_info.dart new file mode 100644 index 000000000..28c704d20 --- /dev/null +++ b/cw_tron/lib/tron_transaction_info.dart @@ -0,0 +1,93 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:on_chain/on_chain.dart' as onchain; +import 'package:on_chain/tron/tron.dart'; + +class TronTransactionInfo extends TransactionInfo { + TronTransactionInfo({ + required this.id, + required this.tronAmount, + required this.txFee, + required this.direction, + required this.blockTime, + required this.to, + required this.from, + required this.isPending, + this.tokenSymbol = 'TRX', + }) : amount = tronAmount.toInt(); + + final String id; + final String? to; + final String? from; + final int amount; + final BigInt tronAmount; + final String tokenSymbol; + final DateTime blockTime; + final bool isPending; + final int? txFee; + final TransactionDirection direction; + + factory TronTransactionInfo.fromJson(Map data) { + return TronTransactionInfo( + id: data['id'] as String, + tronAmount: BigInt.parse(data['tronAmount']), + txFee: data['txFee'], + direction: parseTransactionDirectionFromInt(data['direction'] as int), + blockTime: DateTime.fromMillisecondsSinceEpoch(data['blockTime'] as int), + tokenSymbol: data['tokenSymbol'] as String, + to: data['to'], + from: data['from'], + isPending: data['isPending'], + ); + } + + Map toJson() => { + 'id': id, + 'tronAmount': tronAmount.toString(), + 'txFee': txFee, + 'direction': direction.index, + 'blockTime': blockTime.millisecondsSinceEpoch, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + 'isPending': isPending, + }; + + @override + DateTime get date => blockTime; + + String? _fiatAmount; + + @override + String amountFormatted() { + String formattedAmount = _rawAmountAsString(tronAmount); + + return '$formattedAmount $tokenSymbol'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() { + final formattedFee = onchain.TronHelper.fromSun(BigInt.from(txFee ?? 0)); + + return '$formattedFee TRX'; + } + + String _rawAmountAsString(BigInt amount) { + String formattedAmount = TronHelper.fromSun(amount); + + if (formattedAmount.length >= 8) { + formattedAmount = formattedAmount.substring(0, 8); + } + + return formattedAmount; + } + + String rawTronAmount() => _rawAmountAsString(tronAmount); +} diff --git a/cw_tron/lib/tron_transaction_model.dart b/cw_tron/lib/tron_transaction_model.dart new file mode 100644 index 000000000..1748adc53 --- /dev/null +++ b/cw_tron/lib/tron_transaction_model.dart @@ -0,0 +1,205 @@ +import 'package:blockchain_utils/hex/hex.dart'; +import 'package:on_chain/on_chain.dart'; + +class TronTRC20TransactionModel extends TronTransactionModel { + String? transactionId; + + String? tokenSymbol; + + int? timestamp; + + @override + String? from; + + @override + String? to; + + String? value; + + @override + String get hash => transactionId!; + + @override + DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp ?? 0); + + @override + BigInt? get amount => BigInt.parse(value ?? '0'); + + @override + int? get fee => 0; + + TronTRC20TransactionModel({ + this.transactionId, + this.tokenSymbol, + this.timestamp, + this.from, + this.to, + this.value, + }); + + TronTRC20TransactionModel.fromJson(Map json) { + transactionId = json['transaction_id']; + tokenSymbol = json['token_info'] != null ? json['token_info']['symbol'] : null; + timestamp = json['block_timestamp']; + from = json['from']; + to = json['to']; + value = json['value']; + } +} + +class TronTransactionModel { + List? ret; + String? txID; + int? blockTimestamp; + List? contracts; + + /// Getters to extract out the needed/useful information directly from the model params + /// Without having to go through extra steps in the methods that use this model. + bool get isError { + if (ret?.first.contractRet == null) return true; + + return ret?.first.contractRet != "SUCCESS"; + } + + String get hash => txID!; + + DateTime get date => DateTime.fromMillisecondsSinceEpoch(blockTimestamp ?? 0); + + String? get from => contracts?.first.parameter?.value?.ownerAddress; + + String? get to => contracts?.first.parameter?.value?.receiverAddress; + + BigInt? get amount => contracts?.first.parameter?.value?.txAmount; + + int? get fee => ret?.first.fee; + + String? get contractAddress => contracts?.first.parameter?.value?.contractAddress; + + TronTransactionModel({ + this.ret, + this.txID, + this.blockTimestamp, + this.contracts, + }); + + TronTransactionModel.fromJson(Map json) { + if (json['ret'] != null) { + ret = []; + json['ret'].forEach((v) { + ret!.add(Ret.fromJson(v)); + }); + } + txID = json['txID']; + blockTimestamp = json['block_timestamp']; + contracts = json['raw_data'] != null + ? (json['raw_data']['contract'] as List) + .map((e) => Contract.fromJson(e as Map)) + .toList() + : null; + } +} + +class Ret { + String? contractRet; + int? fee; + + Ret({this.contractRet, this.fee}); + + Ret.fromJson(Map json) { + contractRet = json['contractRet']; + fee = json['fee']; + } +} + +class Contract { + Parameter? parameter; + String? type; + + Contract({this.parameter, this.type}); + + Contract.fromJson(Map json) { + parameter = json['parameter'] != null ? Parameter.fromJson(json['parameter']) : null; + type = json['type']; + } +} + +class Parameter { + Value? value; + String? typeUrl; + + Parameter({this.value, this.typeUrl}); + + Parameter.fromJson(Map json) { + value = json['value'] != null ? Value.fromJson(json['value']) : null; + typeUrl = json['type_url']; + } +} + +class Value { + String? data; + String? ownerAddress; + String? contractAddress; + int? amount; + String? toAddress; + String? assetName; + + //Getters to extract address for tron transactions + /// If the contract address is null, it returns the toAddress + /// If it's not null, it decodes the data field and gets the receiver address. + String? get receiverAddress { + if (contractAddress == null) return toAddress; + + if (data == null) return null; + + return _decodeAddressFromEncodedDataField(data!); + } + + //Getters to extract amount for tron transactions + /// If the contract address is null, it returns the amount + /// If it's not null, it decodes the data field and gets the tx amount. + BigInt? get txAmount { + if (contractAddress == null) return BigInt.from(amount ?? 0); + + if (data == null) return null; + + return _decodeAmountInvolvedFromEncodedDataField(data!); + } + + Value( + {this.data, + this.ownerAddress, + this.contractAddress, + this.amount, + this.toAddress, + this.assetName}); + + Value.fromJson(Map json) { + data = json['data']; + ownerAddress = json['owner_address']; + contractAddress = json['contract_address']; + amount = json['amount']; + toAddress = json['to_address']; + assetName = json['asset_name']; + } + + /// To get the address from the encoded data field + String _decodeAddressFromEncodedDataField(String output) { + // To get the receiver address from the encoded params + output = output.replaceFirst('0x', '').substring(8); + final abiCoder = ABICoder.fromType('address'); + final decoded = abiCoder.decode(AbiParameter.bytes, hex.decode(output)); + final tronAddress = TronAddress.fromEthAddress((decoded.result as ETHAddress).toBytes()); + + return tronAddress.toString(); + } + + /// To get the amount from the encoded data field + BigInt _decodeAmountInvolvedFromEncodedDataField(String output) { + output = output.replaceFirst('0x', '').substring(72); + final amountAbiCoder = ABICoder.fromType('uint256'); + final decodedA = amountAbiCoder.decode(AbiParameter.uint256, hex.decode(output)); + final amount = decodedA.result as BigInt; + + return amount; + } +} diff --git a/cw_tron/lib/tron_wallet.dart b/cw_tron/lib/tron_wallet.dart new file mode 100644 index 000000000..a798f343a --- /dev/null +++ b/cw_tron/lib/tron_wallet.dart @@ -0,0 +1,560 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:blockchain_utils/blockchain_utils.dart'; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_tron/default_tron_tokens.dart'; +import 'package:cw_tron/file.dart'; +import 'package:cw_tron/tron_abi.dart'; +import 'package:cw_tron/tron_balance.dart'; +import 'package:cw_tron/tron_client.dart'; +import 'package:cw_tron/tron_exception.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_transaction_credentials.dart'; +import 'package:cw_tron/tron_transaction_history.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:cw_tron/tron_wallet_addresses.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:on_chain/on_chain.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'tron_wallet.g.dart'; + +class TronWallet = TronWalletBase with _$TronWallet; + +abstract class TronWalletBase + extends WalletBase with Store { + TronWalletBase({ + required WalletInfo walletInfo, + String? mnemonic, + String? privateKey, + required String password, + TronBalance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _client = TronClient(), + walletAddresses = TronWalletAddresses(walletInfo), + balance = ObservableMap.of( + {CryptoCurrency.trx: initialBalance ?? TronBalance(BigInt.zero)}, + ), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = TronTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(TronToken.typeId)) { + CakeHive.registerAdapter(TronTokenAdapter()); + } + + sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String? _mnemonic; + final String? _hexPrivateKey; + final String _password; + + late final Box tronTokensBox; + + late final TronPrivateKey _tronPrivateKey; + + late final TronPublicKey _tronPublicKey; + + TronPublicKey get tronPublicKey => _tronPublicKey; + + TronPrivateKey get tronPrivateKey => _tronPrivateKey; + + late String _tronAddress; + + late TronClient _client; + + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @observable + String? nativeTxEstimatedFee; + + @observable + String? trc20EstimatedFee; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + Completer sharedPrefs = Completer(); + + Future init() async { + await initTronTokensBox(); + + await walletAddresses.init(); + await transactionHistory.init(); + _tronPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + + _tronPublicKey = _tronPrivateKey.publicKey(); + + _tronAddress = _tronPublicKey.toAddress().toString(); + + walletAddresses.address = _tronAddress; + + await save(); + } + + static Future open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String?; + final privateKey = data['private_key'] as String?; + final balance = TronBalance.fromJSON(data['balance'] as String) ?? TronBalance(BigInt.zero); + + return TronWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + ); + } + + void addInitialTokens() { + final initialTronTokens = DefaultTronTokens().initialTronTokens; + + for (var token in initialTronTokens) { + tronTokensBox.put(token.contractAddress, token); + } + } + + Future initTronTokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}"; + + tronTokensBox = await CakeHive.openBox(boxName); + } + + String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; + + Future getPrivateKey({ + String? mnemonic, + String? privateKey, + required String password, + }) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + return TronPrivateKey(privateKey); + } + + final seed = bip39.mnemonicToSeed(mnemonic!); + + // Derive a TRON private key from the seed + final bip44 = Bip44.fromSeed(seed, Bip44Coins.tron); + + final childKey = bip44.deriveDefaultPath; + + return TronPrivateKey.fromBytes(childKey.privateKey.raw); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed"); + } + + _getEstimatedFees(); + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future _getEstimatedFees() async { + final nativeFee = await _getNativeTxFee(); + nativeTxEstimatedFee = TronHelper.fromSun(BigInt.from(nativeFee)); + + final trc20Fee = await _getTrc20TxFee(); + trc20EstimatedFee = TronHelper.fromSun(BigInt.from(trc20Fee)); + + log('Native Estimated Fee: $nativeTxEstimatedFee'); + log('TRC20 Estimated Fee: $trc20EstimatedFee'); + } + + Future _getNativeTxFee() async { + try { + final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress()); + return fee; + } catch (e) { + log(e.toString()); + return 0; + } + } + + Future _getTrc20TxFee() async { + try { + final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress()); + return trc20fee; + } catch (e) { + log(e.toString()); + return 0; + } + } + + @action + @override + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await fetchTransactions(); + fetchTrc20ExcludedTransactions(); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final tronCredentials = credentials as TronTransactionCredentials; + + final outputs = tronCredentials.outputs; + + final hasMultiDestination = outputs.length > 1; + + final CryptoCurrency transactionCurrency = + balance.keys.firstWhere((element) => element.title == tronCredentials.currency.title); + + final walletBalanceForCurrency = balance[transactionCurrency]!.balance; + + BigInt totalAmount = BigInt.zero; + bool shouldSendAll = false; + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw TronTransactionCreationException(transactionCurrency); + } + + final totalAmountFromCredentials = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); + + totalAmount = BigInt.from(totalAmountFromCredentials); + + if (walletBalanceForCurrency < totalAmount) { + throw TronTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + + shouldSendAll = output.sendAll; + + if (shouldSendAll) { + totalAmount = walletBalanceForCurrency; + } else { + final totalOriginalAmount = double.parse(output.cryptoAmount ?? '0.0'); + totalAmount = TronHelper.toSun(totalOriginalAmount.toString()); + } + + if (walletBalanceForCurrency < totalAmount || totalAmount < BigInt.zero) { + throw TronTransactionCreationException(transactionCurrency); + } + } + + final tronBalance = balance[CryptoCurrency.trx]?.balance ?? BigInt.zero; + + final pendingTransaction = await _client.signTransaction( + ownerPrivKey: _tronPrivateKey, + toAddress: tronCredentials.outputs.first.isParsedAddress + ? tronCredentials.outputs.first.extractedAddress! + : tronCredentials.outputs.first.address, + amount: TronHelper.fromSun(totalAmount), + currency: transactionCurrency, + tronBalance: tronBalance, + sendAll: shouldSendAll, + ); + + return pendingTransaction; + } + + @override + Future> fetchTransactions() async { + final address = _tronAddress; + + final transactions = await _client.fetchTransactions(address); + + final Map result = {}; + + final contract = ContractABI.fromJson(trc20Abi, isTron: true); + + final ownerAddress = TronAddress(_tronAddress); + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + String? tokenSymbol; + if (transactionModel.contractAddress != null) { + final tokenAddress = TronAddress(transactionModel.contractAddress!); + + tokenSymbol = (await _client.getTokenDetail( + contract, + "symbol", + ownerAddress, + tokenAddress, + ) as String?) ?? + ''; + } + + result[transactionModel.hash] = TronTransactionInfo( + id: transactionModel.hash, + tronAmount: transactionModel.amount ?? BigInt.zero, + direction: TronAddress(transactionModel.from!, visible: false).toAddress() == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + blockTime: transactionModel.date, + txFee: transactionModel.fee, + tokenSymbol: tokenSymbol ?? "TRX", + to: transactionModel.to, + from: transactionModel.from, + isPending: false, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + + return transactionHistory.transactions; + } + + Future fetchTrc20ExcludedTransactions() async { + final address = _tronAddress; + + final transactions = await _client.fetchTrc20ExcludedTransactions(address); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionHistory.transactions.containsKey(transactionModel.hash)) { + continue; + } + + result[transactionModel.hash] = TronTransactionInfo( + id: transactionModel.hash, + tronAmount: transactionModel.amount ?? BigInt.zero, + direction: transactionModel.from! == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + blockTime: transactionModel.date, + txFee: transactionModel.fee, + tokenSymbol: transactionModel.tokenSymbol ?? "TRX", + to: transactionModel.to, + from: transactionModel.from, + isPending: false, + ); + } + + transactionHistory.addMany(result); + + await transactionHistory.save(); + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String? get seed => _mnemonic; + + @override + String get privateKey => _tronPrivateKey.toHex(); + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + Future _updateBalance() async { + balance[currency] = await _fetchTronBalance(); + + await _fetchTronTokenBalances(); + await save(); + } + + Future _fetchTronBalance() async { + final balance = await _client.getBalance(_tronPublicKey.toAddress()); + return TronBalance(balance); + } + + Future _fetchTronTokenBalances() async { + for (var token in tronTokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchTronTokenBalances( + _tronAddress, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future? updateBalance() async => await _updateBalance(); + + List get tronTokenCurrencies => tronTokensBox.values.toList(); + + Future addTronToken(TronToken token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final newToken = TronToken( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "TRX", + iconPath: iconPath, + ); + + await tronTokensBox.put(newToken.contractAddress, newToken); + + if (newToken.enabled) { + balance[newToken] = await _client.fetchTronTokenBalances( + _tronAddress, + newToken.contractAddress, + ); + } else { + balance.remove(newToken); + } + } + + Future deleteTronToken(TronToken token) async { + await token.delete(); + + balance.remove(token); + await _removeTokenTransactionsInHistory(token); + _updateBalance(); + } + + Future _removeTokenTransactionsInHistory(TronToken token) async { + transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title); + await transactionHistory.save(); + } + + Future getTronToken(String contractAddress) async => + await _client.getTronToken(contractAddress, _tronAddress); + + @override + Future renameWalletFiles(String newWalletName) async { + String transactionHistoryFileNameForWallet = 'tron_transactions.json'; + + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionHistoryFileNameForWallet'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionHistoryFileNameForWallet'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) async { + _updateBalance(); + await fetchTransactions(); + fetchTrc20ExcludedTransactions(); + }); + } + + @override + String signMessage(String message, {String? address}) => + _tronPrivateKey.signPersonalMessage(ascii.encode(message)); + + String getTronBase58AddressFromHex(String hexAddress) { + return TronAddress(hexAddress).toAddress(); + } +} diff --git a/cw_tron/lib/tron_wallet_addresses.dart b/cw_tron/lib/tron_wallet_addresses.dart new file mode 100644 index 000000000..35939de26 --- /dev/null +++ b/cw_tron/lib/tron_wallet_addresses.dart @@ -0,0 +1,36 @@ +import 'dart:developer'; + +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'tron_wallet_addresses.g.dart'; + +class TronWalletAddresses = TronWalletAddressesBase with _$TronWalletAddresses; + +abstract class TronWalletAddressesBase extends WalletAddresses with Store { + TronWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + @observable + String address; + + @override + Future init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + log(e.toString()); + } + } +} diff --git a/cw_tron/lib/tron_wallet_creation_credentials.dart b/cw_tron/lib/tron_wallet_creation_credentials.dart new file mode 100644 index 000000000..dc4f389aa --- /dev/null +++ b/cw_tron/lib/tron_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class TronNewWalletCredentials extends WalletCredentials { + TronNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class TronRestoreWalletFromSeedCredentials extends WalletCredentials { + TronRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class TronRestoreWalletFromPrivateKey extends WalletCredentials { + TronRestoreWalletFromPrivateKey( + {required String name, + required String password, + required this.privateKey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String privateKey; +} diff --git a/cw_tron/lib/tron_wallet_service.dart b/cw_tron/lib/tron_wallet_service.dart new file mode 100644 index 000000000..f4e98ee5d --- /dev/null +++ b/cw_tron/lib/tron_wallet_service.dart @@ -0,0 +1,148 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_tron/tron_client.dart'; +import 'package:cw_tron/tron_exception.dart'; +import 'package:cw_tron/tron_wallet.dart'; +import 'package:cw_tron/tron_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; + +class TronWalletService extends WalletService { + TronWalletService(this.walletInfoSource, {required this.client}); + + late TronClient client; + + final Box walletInfoSource; + + @override + WalletType getType() => WalletType.tron; + + @override + Future create( + TronNewWalletCredentials credentials, { + bool? isTestnet, + }) async { + final strength = credentials.seedPhraseLength == 24 ? 256 : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + + final wallet = TronWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + + try { + final wallet = await TronWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + saveBackup(name); + return wallet; + } catch (_) { + await restoreWalletFilesFromBackup(name); + + final wallet = await TronWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + return wallet; + } + } + + @override + Future restoreFromKeys( + TronRestoreWalletFromPrivateKey credentials, { + bool? isTestnet, + }) async { + final wallet = TronWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed( + TronRestoreWalletFromSeedCredentials credentials, { + bool? isTestnet, + }) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw TronMnemonicIsIncorrectException(); + } + + final wallet = TronWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await TronWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + await saveBackup(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } +} diff --git a/cw_tron/pubspec.yaml b/cw_tron/pubspec.yaml new file mode 100644 index 000000000..9d32c4290 --- /dev/null +++ b/cw_tron/pubspec.yaml @@ -0,0 +1,33 @@ +name: cw_tron +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_evm: + path: ../cw_evm + on_chain: ^3.0.1 + blockchain_utils: ^2.1.1 + mobx: ^2.3.0+1 + bip39: ^1.0.6 + hive: ^2.2.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + build_runner: ^2.3.3 + mobx_codegen: ^2.1.1 + hive_generator: ^1.1.3 +flutter: + # assets: + # - images/a_dot_burr.jpeg diff --git a/cw_tron/test/cw_tron_test.dart b/cw_tron/test/cw_tron_test.dart new file mode 100644 index 000000000..55a2b04be --- /dev/null +++ b/cw_tron/test/cw_tron_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_tron/cw_tron.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md new file mode 100644 index 000000000..011e99990 --- /dev/null +++ b/how_to_add_new_wallet_type.md @@ -0,0 +1,300 @@ +# Guide to adding a new wallet type in Cake Wallet + +## Wallet Integration + +**N:B** Throughout this guide, `walletx` refers to the specific wallet type you want to add. If you're adding `BNB` to CakeWallet, then `walletx` for you here is `bnb`. + +**Core Folder/Files Setup** +- Idenitify your core component/package (major project component), which would power the integration e.g web3dart, solana, onchain etc +- Add a new entry to `WalletType` class in `cw_core/wallet_type.dart`. +- Fill out the necessary information int he various functions in the files, concerning the wallet name, the native currency type, symbol etc. +- Go to `cw_core/lib/currency_for_wallet_type.dart`, in the `currencyForWalletType` function, add a case for `walletx`, returning the native cryptocurrency for `walletx`. +- If the cryptocurrency for walletx is not available among the default cryptocurrencies, add a new cryptocurrency entry in `cw_core/lib/cryptocurrency.dart`. +- Add the newly created cryptocurrency name to the list named `all` in this file. +- Create a package for the wallet specific integration, name it. `cw_walletx` +- Add the following initial common files and replicate to fit the wallet + - walletx_transaction_history.dart + - walletx_transaction_info.dart + - walletx_mnemonics_exception.dart + - walletx_tokens.dart + - walletx_wallet_service.dart: + - walletx_wallet.dart + - etc. + +- Add the code to run the code generation needed for the files in the `cw_walletx` package to the `model_generator.sh` script + + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + +- Add the relevant dev_dependencies for generating the files also + - build_runner + - mobx_codegen + - hive_generator + +**WalletX Proxy Setup** + +A `Proxy` class is used to communicate with the specific wallet package we have. Instead of directly making use of methods and parameters in `cw_walletx` within the `lib` directory, we use a proxy to access these data. All important functions, calls and interactions we want to make with our `cw_walletx` package would be defined and done through the proxy class. The class would define the import + +- Create a proxy folder titled `walletx` to handle the wallet operations. It would contain 2 files: `cw_walletx.dart` and `walletx.dart`. +- `cw_walletx.dart` file would hold an implementation class containing major operations to be done in the lib directory. It serves as the link between the cw_walletx package and the rest of the codebase(lib directory files and folders). +- `walletx.dart` would contain the abstract class highlighting the methods that would bring the functionalities and features in the `cw_walletx` package to the rest of the `lib` directory. +- Add `walletx.dart` to `.gitignore` as we won’t be pushing it: `lib/tron/tron.dart`. +- `walletx.dart` would always be generated based on the configure files we would be setting up in the next step. + +**Configuration Files Setup** +- Before we populate the field, head over to `tool/configure.dart` to setup the necessary configurations for the `walletx` proxy. +- Define the output path, it’ll follow the format `lib/walletx/walletx.dart`. +- Add the variable to check if `walletx` is to be activated +- Define the function that would generate the abstract class for the proxy.(We will flesh out this function in the next steps). +- Add the defined variable in step 2 to the `generatePubspec` and `generateWalletTypes`. +- Next, modify the following functions: + - generatePubspec function + 1. Add the parameters to the method params (i.e required bool hasWalletX) + 2. Define a variable to hold the entry for the pubspec.yaml file + + const cwWalletX = """ + cw_tron: + path: ./cw_walletx + """; + + 3. Add an if block that takes in the passed parameter and adds the defined variable(inn the previous step) to the list of outputs + + if (hasWalletX) { + output += '\n$cwWalletX’; + } + + - generateWalletTypes function + 1. Add the parameters to the method params (i.e required bool hasWalletX) + 2. Add an if block to add the wallet type to the list of outputs this function generates + + if (hasWalletX) { + outputContent += '\tWalletType.walletx,\n’; + } + +- Head over to `scripts/android/pubspec.sh` script, and modify the `CONFIG_ARGS` under `$CAKEWALLET`. Add `"—walletx”` to the end of the passed in params. +- Repeat this in `scripts/ios/app_config.sh` and `scripts/macos/app_config.sh` +- Open a terminal and cd into `scripts/android/`. Run the following commands to run setup configuration scripts(proxy class, add walletx to list of wallet types and add cw_walletx to pubspec). + + source ./app_env.sh cakewallet + + ./app_config.sh + + cd cw_walletx && flutter pub get && flutter packages pub run build_runner build + + flutter packages pub run build_runner build --delete-conflicting-outputs + +Moving forward, our interactions with the cw_walletx package would be through the proxy class and its methods. + +**Pre-Wallet Creation for WalletX** +- Go to `di.dart` and locate the block to `registerWalletService`. In this, add the case to handle creating the WalletXWalletService + + case WalletType.walletx: + return walletx!.createWalletXWalletService(_walletInfoSource); + +- Go to `lib/view_model/wallet_new_vm.dart`, in the getCredentials method, which gets the new wallet credentials for walletX add the case for the new wallet + + case WalletType.walletx: + return walletx!.createWalletXNewWalletCredentials(name: name); + +**Node Setup** +- Before we can be able to successfully create a new wallet of wallet type walletx we need to setup the node that the wallet would use: +- In the assets directory, create a new file and name it `walletx_node_list.yml`. This yml file would contain the details for nodes to be used for walletX. An example structure for each node entry + + uri: "api.nodeurl.io" + is_default: true + useSSL: true + +You can add as many node entries as desired. + +- Add the path to the yml file created to the `pubspec_base.yaml` file (`“assets/walletx_node_list.yml”`) +- Go to `lib/entities/node_list.dart`, add a function to load the node entries we made in `walletx_node_list.yml` for walletx. +- Name your function `loadDefaultWalletXNodes()`. The function would handle loading the yml file as a string and parsing it into a Node Object to be used within the app. Here’s a template for the function. + + Future> loadDefaultWalletXNodes() async { + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.tron; + nodes.add(node); + } + } + return nodes; + } + +- Inside the `resetToDefault` function, call the function you created and add the result to the nodes result variable. +- Go to `lib/entities/default_settings_migration.dart` file, we’ll be adding the following to the file. +- At the top of the file, after the imports, define the default nodeUrl for wallet-name. +- Next, write a function to fetch the node for this default uri you added above. + + Node? getWalletXDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == walletXDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.walletx); + } + +- Next, write a function that will add the list of nodes we declared in the `walletx_node_list.yml` file to the Nodes Box, to be used in the app. Here’s the format for this function + + Future addWalletXNodeList({required Box nodes}) async { + final nodeList = await loadDefaultWalletXNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } + } + +- Next, we’ll write the function to change walletX current node to default. An handy function we would make use of later on. Add a new preference key in `lib/entities/preference_key.dart` with the format `PreferencesKey.currentWalletXNodeIdKey`, we’ll use it to identify the current node id. + + Future changeWalletXCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getWalletXDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + await sharedPreferences.setInt(PreferencesKey.currentWalletXNodeIdKey, nodeId); + } + +- Next, in the `defaultSettingsMigration` function at the top of the file, add a new case to handle both `addWalletXNodeList` and `changeWalletXCurrentNodeToDefault` + + case “next-number-increment”: + await addWalletXNodeList(nodes: nodes); + await changeWalletXCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; + +- Next, increase the `initialMigrationVersion` number in `main.dart` to be the new case entry number you entered in the step above for the `defaultSettingsMigration` function. +- Next, go to `lib/view_model/node_list/node_list_view_model.dart` +- In the `reset` function, add a case for walletX: + + case WalletType.tron: + node = getTronDefaultNode(nodes: _nodeSource)!; + break; + +- Lastly, go to `cw_core/lib/node.dart`, +- In the uri getter, add a case to handle the uri setup for walletX. If the node uses http, return `Uri.http`, if not, return `Uri.https` + + case WalletType.walletX: + return Uri.https(uriRaw, ‘’); + +- Also, in the `requestNode` method, add a case for `WalletType.walletx` +- Next is the modifications to `lib/store/settings_store.dart` file: +- In the `load` function, create a variable to fetch the currentWalletxNodeId using the `PreferencesKey.currentWalletXNodeIdKey` we created earlier. +- Create another variable `walletXNode` which gets the walletx node using the nodeId variable assigned in the step above. +- Add a check to see if walletXNode is not null, if it’s not null, assign the created tronNode variable to the nodeMap with a type of walletX + + final walletXNode = nodeSource.get(walletXNodeId); + final walletXNodeId = sharedPreferences.getInt(PreferencesKey.currentWalletXNodeIdKey); + if (walletXNode != null) { + nodes[WalletType.walletx] = walletXNode; + } + +- Repeat the steps above in the `reload` function +- Next, add a case for walletX in the `_saveCurrentNode` function. + +- Run the following commands after to generate modified files in cw_core and lib + + cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + + flutter packages pub run build_runner build --delete-conflicting-outputs + +- Lastly, before we run the app to test what we’ve done so far, +- Go to `lib/src/dashboard/widgets/menu_widget.dart` and add an icon for walletX to be used within the app. +- Go to `lib/src/screens/wallet_list/wallet_list_page.dart` and add an icon for walletx, add a case for walletx also in the `imageFor` method. +- Do the same thing in `lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart` + +- One last thing before we can create a wallet for walletx, go to `lib/view_model/wallet_new_vm.dart` +- Modify the `seedPhraseWordsLength` getter by adding a case for `WalletType.walletx` + +Now you can run the codebase and successfully create a wallet for type walletX successfully. + +**Display Seeds/Keys** +- Next, we want to set up our wallet to display the seeds and/or keys in the security page of the app. +- Go to `lib/view_model/wallet_keys_view_model.dart` +- Modify the `populateItems` function by adding a case for `WalletType.walletx` in it. +- Now your seeds and/or keys should display when you go to Security and Backup -> Show seed/keys page within the app. + +**Restore Wallet** +- Go to `lib/core/seed_validator.dart` +- In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds. +- Next, go to `lib/restore_view_model.dart` +- Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key +- Add a switch case to handle the various restore modes that walletX supports +- Modify the `getCredential` method to handle the restore flows for `WalletType.walletx` +- Run the build_runner code generation command + +**Receive** +- Go to `lib/view_model/wallet_address_list/wallet_address_list_view_model.dart` +- Create an implementation of `PaymentUri` for type WalletX. +- In the uri getter, add a case for `WalletType.walletx` returning the implementation class for `PaymentUri` +- Modify the `addressList` getter to return the address/addresses for walletx + +**Balance Screen** +- Go to `lib/view_model/dashboard/balance_view_model.dart` +- Modify the function to adjust the way the balance is being display on the app: `isHomeScreenSettingsEnabled` +- Add a case to the `availableBalanceLabel` getter to modify the text being displayed (Available or confirmed) +- Same for `additionalBalanceLabel` +- Next, go to `lib/reactions/fiat_rate_update.dart` +- Modify the `startFiatRateUpdate` function and add a check for `WalletType.walletx` to return all the token currencies +- Next, go to `lib/reactions/on_current_wallet_change.dart` +- Modify the `startCurrentWalletChangeReaction` function and add a check for `WalletType.walletx` to return all the token currencies +- Lastly, go to `lib/view_model/dashboard/transaction_list_item.dart` +- In the `formattedFiatAmount` getter, add a case to handle the fiat amount conversion for `WalletType.walletx` + +**Send ViewModel** +- Go to `lib/view_model/send/send_view_model.dart` +- Modify the `_credentials` function to reflect `WalletType.walletx` +- Modify `hasMultipleTokens` to reflect wallets + +**Exchange** +- Go to lib/view_model/exchange/exchange_view_model.dart +- First, add a case for WalletType.walletx in the `initialPairBasedOnWallet` method. +- If WalletX supports tokens, go to `lib/view_model/exchange/exchange_trade_view_model.dart` +- Modify the `_checkIfCanSend` method by creating a `_isWalletXToken` that checks if the from currency is WalletX and if its tag is for walletx +- Add `_isWalletXToken` to the return logic for the method. + +**Secrets** +- Create a json file named `wallet-secrets-config.json` and put an empty curly bracket “{}” in it +- Add a new entry to `tool/utils/secret_key.dart` for walletx +- Modify the `tool/generate_secrets_config.dart` file for walletx, don’t forget to call `secrets.clear()` before adding a new set of generation logic +- Modify the `tool/import_secrets_config.dart` file for walletx +- In the `.gitignore` file, add `**/tool/.walletx-secrets-config.json` and `**/cw_walletx/lib/.secrets.g.dart` + +**HomeSettings: WalletX Tokens Display and Management** +- Go to `lib/view_model/dashboard/home_settings_view_model.dart` +- Modify the `_updateTokensList` method to add all walletx tokens if the wallet type is `WalletType.walletx`. +- Modify the `getTokenAddressBasedOnWallet` method to include a case to fetch the address for a WalletX token. +- Modify the `getToken` method to return a specific walletx token +- Modify the `addToken`, `deleteToken` and `changeTokenAvailability` methods to handle cases where the walletType is walletx + +**Buy and Sell WalletX** +- Go to `lib/entities/provider_types.dart` +- Add a case for `WalletType.walletx` in the `getAvailableBuyProviderTypes` method. Return a list of providers that support buying WalletX. +- Add a case for `WalletType.walletx` in the `getAvailableSellProviderTypes` method. Return a list of providers that support selling WalletX. + +**Restore QR setup** +- Go to `lib/view_model/restore/wallet_restore_from_qr_code.dart` +- Add the scheme for walletx in `_walletTypeMap` +- Also modify `_determineWalletRestoreMode` to include a case for walletx +- Go to `lib/view_model/restore/restore_from_qr_vm.dart` +- Modify `getCredentialsFromRestoredWallet` method +- Go to `lib/core/address_validator.dart` +- Modify the `getAddressFromStringPattern` method to add a case for `WalletType.walletx` +- Add the scheme for walletx for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` + +**Transaction History** +- Go to `lib/view_model/transaction_details_view_model.dart` +- Add a case for `WalletType.walletx` to add the items to be displayed on the detailed view +- Modify the `_explorerUrl` method to add the blockchain explorer link for WalletX in order to view the more info on a transaction +- Modify the `_explorerDescription` to display the name of the explorer + + + + +# Points to note when adding the new wallet type + +1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add that to this function `_checkIfCanSend` in `exchange_trade_view_model.dart` +2. Check On/Off ramp providers that support the new wallet currency and add them accordingly in `provider_types.dart` +3. Add support for wallet uri scheme to restore from QR for both Android in `AndroidManifestBase.xml` and iOS in `InfoBase.plist` +4. Make sure no imports are using the wallet internal package files directly, instead use the proxy layers that is created in the main lib `lib/cw_ethereum.dart` for example. (i.e try building Monero.com if you get compilation errors, then you probably missed something) +5. + + +Copyright (C) 2018-2023 Cake Labs LLC diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index a7f208870..443f9791f 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -190,6 +190,36 @@ solana-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + tron + CFBundleURLSchemes + + tron + + + + CFBundleTypeRole + Viewer + CFBundleURLName + tron-wallet + CFBundleURLSchemes + + tron-wallet + + + + CFBundleTypeRole + Viewer + CFBundleURLName + tron_wallet + CFBundleURLSchemes + + tron_wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 967cf9bf0..01374d5a2 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -294,6 +294,8 @@ class AddressValidator extends TextValidator { '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; case CryptoCurrency.sol: return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.trx: + return '^(T|t)[1-9A-HJ-NP-Za-km-z]{33}\$'; default: if (type.tag == CryptoCurrency.eth.title) { return '0x[0-9a-zA-Z]{42}'; diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 6d04055ba..3e3445757 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -40,6 +41,8 @@ class SeedValidator extends Validator { return polygon!.getPolygonWordList(language); case WalletType.solana: return solana!.getSolanaWordList(language); + case WalletType.tron: + return tron!.getTronWordList(language); default: return []; } diff --git a/lib/di.dart b/lib/di.dart index d78da638c..00710897f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -873,6 +874,8 @@ Future setup({ return polygon!.createPolygonWalletService(_walletInfoSource); case WalletType.solana: return solana!.createSolanaWalletService(_walletInfoSource); + case WalletType.tron: + return tron!.createTronWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 99178c815..94b23d3c9 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -36,6 +36,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; const solanaDefaultNodeUri = 'rpc.ankr.com'; +const tronDefaultNodeUri = 'api.trongrid.io'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; Future defaultSettingsMigration( @@ -207,23 +208,22 @@ Future defaultSettingsMigration( case 28: await _updateMoneroPriority(sharedPreferences); break; - case 29: await changeDefaultBitcoinNode(nodes, sharedPreferences); break; - case 30: await disableServiceStatusFiatDisabled(sharedPreferences); break; - case 31: await updateNanoNodeList(nodes: nodes); break; - case 32: await updateBtcNanoWalletInfos(walletInfoSource); break; - + case 33: + await addTronNodeList(nodes: nodes); + await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; default: break; } @@ -478,6 +478,11 @@ Node? getSolanaDefaultNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); } +Node? getTronDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +} + Future insecureStorageMigration({ required SharedPreferences sharedPreferences, required FlutterSecureStorage secureStorage, @@ -809,6 +814,7 @@ Future checkCurrentNodes( final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -829,6 +835,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId); final currentSolanaNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId); + final currentTronNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -894,6 +902,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); } + + if (currentTronNodeServer == null) { + final node = Node(uri: tronDefaultNodeUri, type: WalletType.tron); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1022,3 +1036,20 @@ Future changeSolanaCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); } + +Future addTronNodeList({required Box nodes}) async { + final nodeList = await loadDefaultTronNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeTronCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getTronDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); +} diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 3c82a3f6c..c1211d2fe 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -166,6 +166,23 @@ Future> loadDefaultSolanaNodes() async { return nodes; } +Future> loadDefaultTronNodes() async { + final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + + node.type = WalletType.tron; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -176,6 +193,7 @@ Future resetToDefault(Box nodeSource) async { final nanoNodes = await loadDefaultNanoNodes(); final polygonNodes = await loadDefaultPolygonNodes(); final solanaNodes = await loadDefaultSolanaNodes(); + final tronNodes = await loadDefaultTronNodes(); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -185,7 +203,7 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes; + solanaNodes + tronNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index f512d6b72..55b5d55a1 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -14,6 +14,7 @@ class PreferencesKey { static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentSolanaNodeIdKey = 'current_node_id_sol'; + static const currentTronNodeIdKey = 'current_node_id_trx'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 5fc0b5566..0151c8115 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -23,10 +23,11 @@ List priorityForWalletType(WalletType type) { return bitcoinCash!.getTransactionPriorities(); case WalletType.polygon: return polygon!.getTransactionPriorities(); - // no such thing for nano/banano/solana: + // no such thing for nano/banano/solana/tron: case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: return []; default: return []; diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index ca168a299..37a492987 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -69,6 +69,13 @@ class ProvidersHelper { return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; case WalletType.solana: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; + case WalletType.tron: + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.robinhood, + ProviderType.moonpay, + ]; case WalletType.none: case WalletType.haven: return []; @@ -96,6 +103,12 @@ class ProvidersHelper { ProviderType.robinhood, ProviderType.moonpay, ]; + case WalletType.tron: + return [ + ProviderType.askEachTime, + ProviderType.robinhood, + ProviderType.moonpay, + ]; case WalletType.monero: case WalletType.nano: case WalletType.banano: diff --git a/lib/main.dart b/lib/main.dart index ff5b0e5c0..eef8cef62 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -167,7 +167,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 32, + initialMigrationVersion: 33, ); } diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index fb1d4cd1a..e46ef4b64 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; @@ -53,6 +54,11 @@ Future startFiatRateUpdate( solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } + if (appStore.wallet!.type == WalletType.tron) { + currencies = + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index a2f2491f1..a6ce2bae9 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -4,8 +4,8 @@ import 'package:cake_wallet/entities/update_haven_rate.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -70,8 +70,10 @@ void startCurrentWalletChangeReaction( .get() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); - if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) { + if (wallet.type == WalletType.monero || + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } @@ -124,7 +126,11 @@ void startCurrentWalletChangeReaction( currencies = solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); } - + if (wallet.type == WalletType.tron) { + currencies = + tron!.getTronTokenCurrencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { () async { diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 17a22a88f..18ab9d030 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -38,6 +38,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -156,6 +157,8 @@ class _DesktopWalletSelectionDropDownState extends State TransactionRow( - onTap: () => Navigator.of(context) - .pushNamed(Routes.transactionDetails, arguments: transaction), - direction: transaction.direction, - formattedDate: DateFormat('HH:mm').format(transaction.date), - formattedAmount: item.formattedCryptoAmount, - formattedFiatAmount: - dashboardViewModel.balanceViewModel.isFiatDisabled - ? '' - : item.formattedFiatAmount, - isPending: transaction.isPending, - title: item.formattedTitle + item.formattedStatus)); + builder: (_) => TransactionRow( + onTap: () => Navigator.of(context) + .pushNamed(Routes.transactionDetails, arguments: transaction), + direction: transaction.direction, + formattedDate: DateFormat('HH:mm').format(transaction.date), + formattedAmount: item.formattedCryptoAmount, + formattedFiatAmount: + dashboardViewModel.balanceViewModel.isFiatDisabled + ? '' + : item.formattedFiatAmount, + isPending: transaction.isPending, + title: item.formattedTitle + item.formattedStatus, + ), + ); } if (item is AnonpayTransactionListItem) { diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index acd666025..d9e03dbf9 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -34,7 +34,8 @@ class MenuWidgetState extends State { this.bananoIcon = Image.asset('assets/images/nano_icon.png'), this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'), this.polygonIcon = Image.asset('assets/images/matic_icon.png'), - this.solanaIcon = Image.asset('assets/images/sol_icon.png'); + this.solanaIcon = Image.asset('assets/images/sol_icon.png'), + this.tronIcon = Image.asset('assets/images/trx_icon.png'); final largeScreen = 731; @@ -57,6 +58,7 @@ class MenuWidgetState extends State { Image bananoIcon; Image polygonIcon; Image solanaIcon; + Image tronIcon; @override void initState() { @@ -226,6 +228,8 @@ class MenuWidgetState extends State { return polygonIcon; case WalletType.solana: return solanaIcon; + case WalletType.tron: + return tronIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 81c78b1ab..601f5d878 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -104,6 +104,7 @@ class WalletListBodyState extends State { final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24); final solanaIcon = Image.asset('assets/images/sol_icon.png', height: 24, width: 24); + final tronIcon = Image.asset('assets/images/trx_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -316,6 +317,8 @@ class WalletListBodyState extends State { return polygonIcon; case WalletType.solana: return solanaIcon; + case WalletType.tron: + return tronIcon; default: return nonWalletTypeIcon; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 165c72242..607551827 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -872,6 +872,7 @@ abstract class SettingsStoreBase with Store { final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -882,6 +883,7 @@ abstract class SettingsStoreBase with Store { final nanoNode = nodeSource.get(nanoNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); + final tronNode = nodeSource.get(tronNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -944,6 +946,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.solana] = solanaNode; } + if (tronNode != null) { + nodes[WalletType.tron] = tronNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1238,6 +1244,7 @@ abstract class SettingsStoreBase with Store { final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); + final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1247,6 +1254,7 @@ abstract class SettingsStoreBase with Store { final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); + final tronNode = nodeSource.get(tronNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; } @@ -1283,6 +1291,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.solana] = solanaNode; } + if (tronNode != null) { + nodes[WalletType.tron] = tronNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1413,6 +1425,9 @@ abstract class SettingsStoreBase with Store { case WalletType.solana: await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); break; + case WalletType.tron: + await _sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/tron/cw_tron.dart b/lib/tron/cw_tron.dart new file mode 100644 index 000000000..6e4b0a7b0 --- /dev/null +++ b/lib/tron/cw_tron.dart @@ -0,0 +1,114 @@ +part of 'tron.dart'; + +class CWTron extends Tron { + @override + List getTronWordList(String language) => EVMChainMnemonics.englishWordlist; + + WalletService createTronWalletService(Box walletInfoSource) => + TronWalletService(walletInfoSource, client: TronClient()); + + @override + WalletCredentials createTronNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + TronNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createTronRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + }) => + TronRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + + @override + WalletCredentials createTronRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + TronRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + String getAddress(WalletBase wallet) => (wallet as TronWallet).walletAddresses.address; + + Object createTronTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }) => + TronTransactionCredentials( + outputs + .map( + (out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount, + ), + ) + .toList(), + currency: currency, + ); + + @override + List getTronTokenCurrencies(WalletBase wallet) => + (wallet as TronWallet).tronTokenCurrencies; + + @override + Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress) async { + final tronToken = TronToken( + name: token.name, + symbol: token.title, + contractAddress: contractAddress, + decimal: token.decimals, + enabled: token.enabled, + iconPath: token.iconPath, + ); + await (wallet as TronWallet).addTronToken(tronToken); + } + + @override + Future deleteTronToken(WalletBase wallet, CryptoCurrency token) async => + await (wallet as TronWallet).deleteTronToken(token as TronToken); + + @override + Future getTronToken(WalletBase wallet, String contractAddress) async => + (wallet as TronWallet).getTronToken(contractAddress); + + @override + double getTransactionAmountRaw(TransactionInfo transactionInfo) { + final amount = (transactionInfo as TronTransactionInfo).rawTronAmount(); + return double.parse(amount); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as TronTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.trx.title) { + return CryptoCurrency.trx; + } + + wallet as TronWallet; + return wallet.tronTokenCurrencies.firstWhere( + (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase()); + } + + @override + String getTokenAddress(CryptoCurrency asset) => (asset as TronToken).contractAddress; + + @override + String getTronBase58Address(String hexAddress, WalletBase wallet) => + (wallet as TronWallet).getTronBase58AddressFromHex(hexAddress); + + @override + String? getTronNativeEstimatedFee(WalletBase wallet) => + (wallet as TronWallet).nativeTxEstimatedFee; + + @override + String? getTronTRC20EstimatedFee(WalletBase wallet) => (wallet as TronWallet).trc20EstimatedFee; +} diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index a17ddff36..c87e097c3 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -38,6 +38,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return true; case WalletType.monero: case WalletType.none: diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index eee53516e..6f4db52a6 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -80,7 +80,9 @@ abstract class BalanceViewModelBase with Store { @computed bool get isHomeScreenSettingsEnabled => - isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; + isEVMCompatibleChain(wallet.type) || + wallet.type == WalletType.solana || + wallet.type == WalletType.tron; @computed bool get hasAccounts => wallet.type == WalletType.monero; @@ -126,6 +128,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -140,6 +143,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return S.current.xmr_full_balance; case WalletType.nano: case WalletType.banano: @@ -287,6 +291,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: case WalletType.solana: + case WalletType.tron: return false; default: return true; diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e60a37ccf..9e3be746e 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; @@ -79,6 +80,10 @@ abstract class HomeSettingsViewModelBase with Store { ); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress); + } + _updateTokensList(); _updateFiatPrices(token); } @@ -96,6 +101,9 @@ abstract class HomeSettingsViewModelBase with Store { await solana!.deleteSPLToken(_balanceViewModel.wallet, token); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + await tron!.deleteTronToken(_balanceViewModel.wallet, token); + } _updateTokensList(); } @@ -112,6 +120,10 @@ abstract class HomeSettingsViewModelBase with Store { return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress); + } + return null; } @@ -143,6 +155,11 @@ abstract class HomeSettingsViewModelBase with Store { solana!.addSPLToken(_balanceViewModel.wallet, token, address); } + if (_balanceViewModel.wallet.type == WalletType.tron) { + final address = tron!.getTokenAddress(token); + tron!.addTronToken(_balanceViewModel.wallet, token, address); + } + _refreshTokensList(); } @@ -189,6 +206,14 @@ abstract class HomeSettingsViewModelBase with Store { .toList() ..sort(_sortFunc)); } + + if (_balanceViewModel.wallet.type == WalletType.tron) { + tokens.addAll(tron! + .getTronTokenCurrencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action @@ -207,7 +232,7 @@ abstract class HomeSettingsViewModelBase with Store { bool _matchesSearchText(CryptoCurrency asset) { final address = getTokenAddressBasedOnWallet(asset); - // The homes settings would only be displayed for either of Ethereum, Polygon or Solana Wallets. + // The homes settings would only be displayed for either of Tron, Ethereum, Polygon or Solana Wallets. if (address == null) return false; return searchText.isEmpty || @@ -217,6 +242,10 @@ abstract class HomeSettingsViewModelBase with Store { } String? getTokenAddressBasedOnWallet(CryptoCurrency asset) { + if (_balanceViewModel.wallet.type == WalletType.tron) { + return tron!.getTokenAddress(asset); + } + if (_balanceViewModel.wallet.type == WalletType.solana) { return solana!.getTokenAddress(asset); } @@ -229,7 +258,7 @@ abstract class HomeSettingsViewModelBase with Store { return polygon!.getTokenAddress(asset); } - // We return null if it's neither Polygin, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). + // We return null if it's neither Tron, Polygon, Ethereum or Solana wallet (which is actually impossible because we only display home settings for either of these three wallets). return null; } } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 99de14a18..fb5348a29 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -4,7 +4,10 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -34,6 +37,11 @@ class TransactionListItem extends ActionListItem with Keyable { @override dynamic get keyIndex => transaction.id; + bool get hasTokens => + isEVMCompatibleChain(balanceViewModel.wallet.type) || + balanceViewModel.wallet.type == WalletType.solana || + balanceViewModel.wallet.type == WalletType.tron; + String get formattedCryptoAmount { return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); } @@ -63,6 +71,34 @@ class TransactionListItem extends ActionListItem with Keyable { return transaction.isPending ? S.current.pending : ''; } + CryptoCurrency? get assetOfTransaction { + try { + if (balanceViewModel.wallet.type == WalletType.ethereum) { + final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.polygon) { + final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.solana) { + final asset = solana!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + + if (balanceViewModel.wallet.type == WalletType.tron) { + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); + return asset; + } + } catch (e) { + return null; + } + + return null; + } + String get formattedFiatAmount { var amount = ''; @@ -114,6 +150,16 @@ class TransactionListItem extends ActionListItem with Keyable { price: price, ); break; + + case WalletType.tron: + final asset = tron!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + final cryptoAmount = tron!.getTransactionAmountRaw(transaction); + amount = calculateFiatAmountRaw( + cryptoAmount: cryptoAmount, + price: price, + ); + break; default: break; } diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 9bd9ef913..94fec2fa2 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -178,6 +178,10 @@ abstract class ExchangeTradeViewModelBase with Store { wallet.currency == CryptoCurrency.maticpoly && tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag; + bool _isTronToken() => + wallet.currency == CryptoCurrency.trx && + tradesStore.trade!.from.tag == CryptoCurrency.trx.title; + bool _isSplToken() => wallet.currency == CryptoCurrency.sol && tradesStore.trade!.from.tag == CryptoCurrency.sol.title; @@ -186,6 +190,7 @@ abstract class ExchangeTradeViewModelBase with Store { tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || _isEthToken() || _isPolygonToken() || - _isSplToken(); + _isSplToken() || + _isTronToken(); } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 4e5902faa..e5533f48a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -676,6 +676,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.sol; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.tron: + depositCurrency = CryptoCurrency.trx; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 7fe3d1c98..000c9bdea 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -76,6 +76,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.solana: case WalletType.banano: case WalletType.nano: + case WalletType.tron: return true; case WalletType.none: case WalletType.monero: diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9c2d2611e..a7fe9c6ca 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -82,6 +82,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.solana: node = getSolanaDefaultNode(nodes: _nodeSource)!; break; + case WalletType.tron: + node = getTronDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index b9b493f04..1e9aea2c2 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -4,13 +4,14 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/core/generate_wallet_password.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cw_core/wallet_credentials.dart'; @@ -86,6 +87,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.solana: return solana!.createSolanaRestoreWalletFromPrivateKey( name: name, password: password, privateKey: restoreWallet.privateKey!); + case WalletType.tron: + return tron!.createTronRestoreWalletFromPrivateKey( + name: name, password: password, privateKey: restoreWallet.privateKey!); default: throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); } @@ -130,6 +134,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.solana: return solana!.createSolanaRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.tron: + return tron!.createTronRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 925c08cca..09b5c9d96 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -33,6 +33,9 @@ class WalletRestoreFromQRCode { 'bitcoincash-wallet': WalletType.bitcoinCash, 'bitcoincash_wallet': WalletType.bitcoinCash, 'solana-wallet': WalletType.solana, + 'tron': WalletType.tron, + 'tron-wallet': WalletType.tron, + 'tron_wallet': WalletType.tron, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -184,6 +187,14 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.keys; } + if (type == WalletType.tron && credentials.containsKey('private_key')) { + final privateKey = credentials['private_key'] as String; + if (privateKey.isEmpty) { + throw Exception('Unexpected restore mode: private_key'); + } + return WalletRestoreMode.keys; + } + throw Exception('Unexpected restore mode: restore params are invalid'); } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 07d98ff32..a79baea48 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -8,6 +8,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -117,6 +118,17 @@ abstract class OutputBase with Store { @computed double get estimatedFee { try { + if (_wallet.type == WalletType.tron) { + if (cryptoCurrencyHandler() == CryptoCurrency.trx) { + final nativeEstimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? 0; + return double.parse(nativeEstimatedFee.toString()); + } else { + final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0; + return double.parse(trc20EstimatedFee.toString()); + } + + } + if (_wallet.type == WalletType.solana) { return solana!.getEstimateFees(_wallet) ?? 0.0; } @@ -163,8 +175,11 @@ abstract class OutputBase with Store { @computed String get estimatedFeeFiatAmount { try { - final currency = - isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler(); + final currency = (isEVMCompatibleChain(_wallet.type) || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron) + ? _wallet.currency + : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); return fiat; @@ -269,6 +284,9 @@ abstract class OutputBase with Store { case WalletType.solana: maximumFractionDigits = 12; break; + case WalletType.tron: + maximumFractionDigits = 12; + break; default: break; } diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index f79fbddc7..66a3c37c8 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -53,7 +53,8 @@ abstract class SendTemplateViewModelBase with Store { _wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum && _wallet.type != WalletType.polygon && - _wallet.type != WalletType.solana; + _wallet.type != WalletType.solana && + _wallet.type != WalletType.tron; @computed CryptoCurrency get cryptoCurrency => _wallet.currency; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 6c0c3870b..bafc1317d 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/exceptions.dart'; @@ -50,7 +51,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor void onWalletChange(wallet) { currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; - hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; + hasMultipleTokens = isEVMCompatibleChain(wallet.type) || + wallet.type == WalletType.solana || + wallet.type == WalletType.tron; } SendViewModelBase( @@ -64,7 +67,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || - appStore.wallet!.type == WalletType.solana, + appStore.wallet!.type == WalletType.solana || + appStore.wallet!.type == WalletType.tron, outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -110,6 +114,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor @computed bool get isBatchSending => outputs.length > 1; + bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron; + @computed String get pendingTransactionFiatAmount { if (pendingTransaction == null) { @@ -236,7 +242,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get hasFeesPriority => wallet.type != WalletType.nano && wallet.type != WalletType.banano && - wallet.type != WalletType.solana; + wallet.type != WalletType.solana && + wallet.type != WalletType.tron; + @observable CryptoCurrency selectedCryptoCurrency; @@ -423,7 +431,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor Object _credentials() { final priority = _settingsStore.priority[wallet.type]; - if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana) { + if (priority == null && wallet.type != WalletType.nano && wallet.type != WalletType.banano && wallet.type != WalletType.solana && + wallet.type != WalletType.tron) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -453,6 +462,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.solana: return solana! .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); + case WalletType.tron: + return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 0493acf81..7e751a920 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -56,8 +56,9 @@ abstract class OtherSettingsViewModelBase with Store { _wallet.type == WalletType.nano || _wallet.type == WalletType.banano; @computed - bool get displayTransactionPriority => - !(changeRepresentativeEnabled || _wallet.type == WalletType.solana); + bool get displayTransactionPriority => !(changeRepresentativeEnabled || + _wallet.type == WalletType.solana || + _wallet.type == WalletType.tron); @computed bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index faa49dfc4..526ff0335 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,3 +1,7 @@ +import 'package:cake_wallet/tron/tron.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -14,10 +18,7 @@ import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; import 'package:collection/collection.dart'; import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; import 'package:intl/src/intl/date_format.dart'; import 'package:mobx/mobx.dart'; @@ -71,6 +72,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.solana: _addSolanaListItems(tx, dateFormat); break; + case WalletType.tron: + _addTronListItems(tx, dateFormat); + break; default: break; } @@ -160,6 +164,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://polygonscan.com/tx/${txId}'; case WalletType.solana: return 'https://solscan.io/tx/${txId}'; + case WalletType.tron: + return 'https://tronscan.org/#/transaction/${txId}'; default: return ''; } @@ -186,6 +192,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'polygonscan.com'; case WalletType.solana: return S.current.view_transaction_on + 'solscan.io'; + case WalletType.tron: + return S.current.view_transaction_on + 'tronscan.org'; default: return ''; } @@ -339,20 +347,19 @@ abstract class TransactionDetailsViewModelBase with Store { transactionInfo.inputAddresses?.length ?? 1, transactionInfo.outputAddresses?.length ?? 1); - RBFListItems.add(StandartListItem( - title: S.current.old_fee, - value: tx.feeFormatted() ?? '0.0')); + RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0')); final priorities = priorityForWalletType(wallet.type); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); - final customItem = priorities.firstWhereOrNull( - (element) => element == sendViewModel.bitcoinTransactionPriorityCustom); + final customItem = priorities + .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom); final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); RBFListItems.add(StandardPickerListItem( title: S.current.estimated_new_fee, - value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}', + value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + + ' ${walletTypeToCryptoCurrency(wallet.type)}', items: priorityForWalletType(wallet.type), customValue: settingsStore.customBitcoinFeeRate.toDouble(), maxValue: maxCustomFeeRate, @@ -378,6 +385,27 @@ abstract class TransactionDetailsViewModelBase with Store { } } + void _addTronListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tron!.getTronBase58Address(tx.to!, wallet)), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tron!.getTronBase58Address(tx.from!, wallet)), + ]; + + items.addAll(_items); + } + @action Future _checkForRBF() async { if (wallet.type == WalletType.bitcoin && @@ -392,10 +420,10 @@ abstract class TransactionDetailsViewModelBase with Store { newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null ? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount) : bitcoin!.getFeeAmountForPriority( - wallet, - priority, - transactionInfo.inputAddresses?.length ?? 1, - transactionInfo.outputAddresses?.length ?? 1); + wallet, + priority, + transactionInfo.inputAddresses?.length ?? 1, + transactionInfo.outputAddresses?.length ?? 1); return bitcoin!.formatterBitcoinAmountToString(amount: newFee); } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 20980f5f0..f6e1359e1 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; @@ -175,6 +176,21 @@ class SolanaURI extends PaymentURI { } } +class TronURI extends PaymentURI { + TronURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'tron:' + address; + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -273,6 +289,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return SolanaURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.tron) { + return TronURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -348,6 +368,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.tron) { + final primaryAddress = tron!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index c33c85504..6b5ae5559 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -118,7 +118,8 @@ abstract class WalletKeysViewModelBase with Store { } if (isEVMCompatibleChain(_appStore.wallet!.type) || - _appStore.wallet!.type == WalletType.solana) { + _appStore.wallet!.type == WalletType.solana || + _appStore.wallet!.type == WalletType.tron) { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), @@ -175,6 +176,8 @@ abstract class WalletKeysViewModelBase with Store { return 'polygon-wallet'; case WalletType.solana: return 'solana-wallet'; + case WalletType.tron: + return 'tron-wallet'; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 8b19108ec..e19efabc5 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -43,6 +44,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return 16; } return 25; + case WalletType.tron: case WalletType.solana: case WalletType.polygon: case WalletType.ethereum: @@ -79,6 +81,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return polygon!.createPolygonNewWalletCredentials(name: name); case WalletType.solana: return solana!.createSolanaNewWalletCredentials(name: name); + case WalletType.tron: + return tron!.createTronNewWalletCredentials(name: name); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 21339f1ae..e19a83bc3 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -6,6 +6,7 @@ import 'package:cw_core/nano_account_info_response.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/tron/tron.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -34,7 +35,8 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { type == WalletType.polygon || type == WalletType.nano || type == WalletType.banano || - type == WalletType.solana, + type == WalletType.solana || + type == WalletType.tron, isButtonEnabled = false, mode = WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { @@ -48,6 +50,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.nano: case WalletType.banano: case WalletType.solana: + case WalletType.tron: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; default: @@ -127,6 +130,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, ); + case WalletType.tron: + return tron!.createTronRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); default: break; } @@ -185,6 +194,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, privateKey: options['private_key'] as String, ); + case WalletType.tron: + return tron!.createTronRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); default: break; } diff --git a/model_generator.sh b/model_generator.sh index 8a6098621..ee88644b6 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -6,6 +6,7 @@ cd cw_haven && flutter pub get && flutter packages pub run build_runner build -- cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_tron && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_ethereum && flutter pub get && cd .. cd cw_polygon && flutter pub get && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 3ec3e7978..bdfa70964 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -158,6 +158,7 @@ flutter: - assets/nano_pow_node_list.yml - assets/polygon_node_list.yml - assets/solana_node_list.yml + - assets/tron_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index d238052fe..bc7985506 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 9f59d6632..ab7fbd422 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -28,7 +28,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron" ;; $HAVEN) diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bd1417c4b..a1143bb12 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/tool/configure.dart b/tool/configure.dart index ceb0c9ccc..f136c9a2a 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -8,6 +8,7 @@ const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart'; const nanoOutputPath = 'lib/nano/nano.dart'; const polygonOutputPath = 'lib/polygon/polygon.dart'; const solanaOutputPath = 'lib/solana/solana.dart'; +const tronOutputPath = 'lib/tron/tron.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -23,6 +24,7 @@ Future main(List args) async { final hasBanano = args.contains('${prefix}banano'); final hasPolygon = args.contains('${prefix}polygon'); final hasSolana = args.contains('${prefix}solana'); + final hasTron = args.contains('${prefix}tron'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); @@ -32,6 +34,7 @@ Future main(List args) async { await generateNano(hasNano); await generatePolygon(hasPolygon); await generateSolana(hasSolana); + await generateTron(hasTron); // await generateBanano(hasEthereum); await generatePubspec( @@ -44,6 +47,7 @@ Future main(List args) async { hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, hasSolana: hasSolana, + hasTron: hasTron, ); await generateWalletTypes( hasMonero: hasMonero, @@ -55,6 +59,7 @@ Future main(List args) async { hasBitcoinCash: hasBitcoinCash, hasPolygon: hasPolygon, hasSolana: hasSolana, + hasTron: hasTron, ); } @@ -1024,6 +1029,79 @@ abstract class Solana { await outputFile.writeAsString(output); } +Future generateTron(bool hasImplementation) async { + final outputFile = File(tronOutputPath); + const tronCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; + +"""; + const tronCWHeaders = """ +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_tron/tron_transaction_credentials.dart'; +import 'package:cw_tron/tron_transaction_info.dart'; +import 'package:cw_tron/tron_wallet_creation_credentials.dart'; + +import 'package:cw_tron/tron_client.dart'; +import 'package:cw_tron/tron_token.dart'; +import 'package:cw_tron/tron_wallet.dart'; +import 'package:cw_tron/tron_wallet_service.dart'; + +"""; + const tronCwPart = "part 'cw_tron.dart';"; + const tronContent = """ +abstract class Tron { + List getTronWordList(String language); + WalletService createTronWalletService(Box walletInfoSource); + WalletCredentials createTronNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createTronRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createTronRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); + String getAddress(WalletBase wallet); + + Object createTronTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + }); + + List getTronTokenCurrencies(WalletBase wallet); + Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress); + Future deleteTronToken(WalletBase wallet, CryptoCurrency token); + Future getTronToken(WalletBase wallet, String contractAddress); + + double getTransactionAmountRaw(TransactionInfo transactionInfo); + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + String getTokenAddress(CryptoCurrency asset); + String getTronBase58Address(String hexAddress, WalletBase wallet); + + String? getTronNativeEstimatedFee(WalletBase wallet); + String? getTronTRC20EstimatedFee(WalletBase wallet); +} + """; + + const tronEmptyDefinition = 'Tron? tron;\n'; + const tronCWDefinition = 'Tron? tron = CWTron();\n'; + + final output = '$tronCommonHeaders\n' + + (hasImplementation ? '$tronCWHeaders\n' : '\n') + + (hasImplementation ? '$tronCwPart\n\n' : '\n') + + (hasImplementation ? tronCWDefinition : tronEmptyDefinition) + + '\n' + + tronContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec( {required bool hasMonero, required bool hasBitcoin, @@ -1033,7 +1111,8 @@ Future generatePubspec( required bool hasBanano, required bool hasBitcoinCash, required bool hasPolygon, - required bool hasSolana}) async { + required bool hasSolana, + required bool hasTron}) async { const cwCore = """ cw_core: path: ./cw_core @@ -1082,6 +1161,10 @@ Future generatePubspec( cw_evm: path: ./cw_evm """; + const cwTron = """ + cw_tron: + path: ./cw_tron + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1121,6 +1204,10 @@ Future generatePubspec( output += '\n$cwSolana'; } + if (hasTron) { + output += '\n$cwTron'; + } + if (hasHaven && !hasMonero) { output += '\n$cwSharedExternal\n$cwHaven'; } else if (hasHaven) { @@ -1152,7 +1239,8 @@ Future generateWalletTypes( required bool hasBanano, required bool hasBitcoinCash, required bool hasPolygon, - required bool hasSolana}) async { + required bool hasSolana, + required bool hasTron}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -1191,6 +1279,10 @@ Future generateWalletTypes( outputContent += '\tWalletType.solana,\n'; } + if (hasTron) { + outputContent += '\tWalletType.tron,\n'; + } + if (hasNano) { outputContent += '\tWalletType.nano,\n'; } diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 8745c2933..6aaa39b7c 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -6,6 +6,7 @@ import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; +const tronConfigPath = 'tool/.tron-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); @@ -20,9 +21,10 @@ Future generateSecretsConfig(List args) async { final configFile = File(configPath); final evmChainsConfigFile = File(evmChainsConfigPath); final solanaConfigFile = File(solanaConfigPath); + final tronConfigFile = File(tronConfigPath); final secrets = {}; - + secrets.addAll(extraInfo); secrets.removeWhere((key, dynamic value) { if (key.contains('--')) { @@ -78,4 +80,18 @@ Future generateSecretsConfig(List args) async { secretsJson = JsonEncoder.withIndent(' ').convert(secrets); await solanaConfigFile.writeAsString(secretsJson); + + secrets.clear(); + + SecretKey.tronSecrets.forEach((sec) { + if (secrets[sec.name] != null) { + return; + } + + secrets[sec.name] = sec.generate(); + }); + + secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + + await tronConfigFile.writeAsString(secretsJson); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 02061669b..b2f3ca691 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -10,6 +10,9 @@ const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart'; const solanaConfigPath = 'tool/.solana-secrets-config.json'; const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; + +const tronConfigPath = 'tool/.tron-secrets-config.json'; +const tronOutputPath = 'cw_tron/lib/.secrets.g.dart'; Future main(List args) async => importSecretsConfig(); Future importSecretsConfig() async { @@ -29,6 +32,11 @@ Future importSecretsConfig() async { final solanaOutput = solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput)); + final tronOutputFile = File(tronOutputPath); + final tronInput = json.decode(File(tronConfigPath).readAsStringSync()) as Map; + final tronOutput = + tronInput.keys.fold('', (String acc, String val) => acc + generateConst(val, tronInput)); + if (outputFile.existsSync()) { await outputFile.delete(); } @@ -46,4 +54,10 @@ Future importSecretsConfig() async { } await solanaOutputFile.writeAsString(solanaOutput); + + if (tronOutputFile.existsSync()) { + await tronOutputFile.delete(); + } + + await tronOutputFile.writeAsString(tronOutput); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 5d5e61cec..542e91b38 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -50,6 +50,10 @@ class SecretKey { SecretKey('ankrApiKey', () => ''), ]; + static final tronSecrets = [ + SecretKey('tronGridApiKey', () => ''), + ]; + final String name; final String Function() generate; }