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
This commit is contained in:
Adegoke David 2024-05-03 19:00:05 +01:00 committed by GitHub
parent e4fd534949
commit d1870ba8b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 3660 additions and 62 deletions

View file

@ -113,6 +113,7 @@ jobs:
touch lib/.secrets.g.dart touch lib/.secrets.g.dart
touch cw_evm/lib/.secrets.g.dart touch cw_evm/lib/.secrets.g.dart
touch cw_solana/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 salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart
echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart
echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart
@ -150,6 +151,7 @@ jobs:
echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart
echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/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 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 - name: Rename app
run: | run: |

3
.gitignore vendored
View file

@ -94,9 +94,11 @@ android/app/key.jks
**/tool/.evm-secrets-config.json **/tool/.evm-secrets-config.json
**/tool/.ethereum-secrets-config.json **/tool/.ethereum-secrets-config.json
**/tool/.solana-secrets-config.json **/tool/.solana-secrets-config.json
**/tool/.tron-secrets-config.json
**/lib/.secrets.g.dart **/lib/.secrets.g.dart
**/cw_evm/lib/.secrets.g.dart **/cw_evm/lib/.secrets.g.dart
**/cw_solana/lib/.secrets.g.dart **/cw_solana/lib/.secrets.g.dart
**/cw_tron/lib/.secrets.g.dart
vendor/ vendor/
@ -132,6 +134,7 @@ lib/bitcoin_cash/bitcoin_cash.dart
lib/nano/nano.dart lib/nano/nano.dart
lib/polygon/polygon.dart lib/polygon/polygon.dart
lib/solana/solana.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_180.png
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png

View file

@ -67,6 +67,9 @@
<data android:scheme="polygon-wallet" /> <data android:scheme="polygon-wallet" />
<data android:scheme="polygon_wallet" /> <data android:scheme="polygon_wallet" />
<data android:scheme="solana-wallet" /> <data android:scheme="solana-wallet" />
<data android:scheme="tron" />
<data android:scheme="tron-wallet" />
<data android:scheme="tron_wallet" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data <meta-data

View file

@ -0,0 +1,4 @@
-
uri: api.trongrid.io
is_default: true
useSSL: true

View file

@ -103,6 +103,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.kaspa, CryptoCurrency.kaspa,
CryptoCurrency.digibyte, CryptoCurrency.digibyte,
CryptoCurrency.usdtSol, CryptoCurrency.usdtSol,
CryptoCurrency.usdcTrc20,
]; ];
static const havenCurrencies = [ static const havenCurrencies = [
@ -217,6 +218,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kas', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); 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 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 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<int, CryptoCurrency> _rawCurrencyMap = static final Map<int, CryptoCurrency> _rawCurrencyMap =

View file

@ -23,7 +23,10 @@ CryptoCurrency currencyForWalletType(WalletType type) {
return CryptoCurrency.maticpoly; return CryptoCurrency.maticpoly;
case WalletType.solana: case WalletType.solana:
return CryptoCurrency.sol; return CryptoCurrency.sol;
case WalletType.tron:
return CryptoCurrency.trx;
default: default:
throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType');
} }
} }

View file

@ -16,3 +16,4 @@ const POW_NODE_TYPE_ID = 14;
const DERIVATION_TYPE_TYPE_ID = 15; const DERIVATION_TYPE_TYPE_ID = 15;
const SPL_TOKEN_TYPE_ID = 16; const SPL_TOKEN_TYPE_ID = 16;
const DERIVATION_INFO_TYPE_ID = 17; const DERIVATION_INFO_TYPE_ID = 17;
const TRON_TOKEN_TYPE_ID = 18;

View file

@ -94,6 +94,7 @@ class Node extends HiveObject with Keyable {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return Uri.https(uriRaw, path ?? ''); return Uri.https(uriRaw, path ?? '');
default: default:
throw Exception('Unexpected type ${type.toString()} for Node uri'); throw Exception('Unexpected type ${type.toString()} for Node uri');
@ -152,6 +153,7 @@ class Node extends HiveObject with Keyable {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return requestElectrumServer(); return requestElectrumServer();
default: default:
return false; return false;

View file

@ -15,6 +15,7 @@ const walletTypes = [
WalletType.banano, WalletType.banano,
WalletType.polygon, WalletType.polygon,
WalletType.solana, WalletType.solana,
WalletType.tron,
]; ];
@HiveType(typeId: WALLET_TYPE_TYPE_ID) @HiveType(typeId: WALLET_TYPE_TYPE_ID)
@ -50,7 +51,10 @@ enum WalletType {
polygon, polygon,
@HiveField(10) @HiveField(10)
solana solana,
@HiveField(11)
tron
} }
int serializeToInt(WalletType type) { int serializeToInt(WalletType type) {
@ -75,6 +79,8 @@ int serializeToInt(WalletType type) {
return 8; return 8;
case WalletType.solana: case WalletType.solana:
return 9; return 9;
case WalletType.tron:
return 10;
default: default:
return -1; return -1;
} }
@ -102,6 +108,8 @@ WalletType deserializeFromInt(int raw) {
return WalletType.polygon; return WalletType.polygon;
case 9: case 9:
return WalletType.solana; return WalletType.solana;
case 10:
return WalletType.tron;
default: default:
throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); throw Exception('Unexpected token: $raw for WalletType deserializeFromInt');
} }
@ -129,6 +137,8 @@ String walletTypeToString(WalletType type) {
return 'Polygon'; return 'Polygon';
case WalletType.solana: case WalletType.solana:
return 'Solana'; return 'Solana';
case WalletType.tron:
return 'Tron';
default: default:
return ''; return '';
} }
@ -156,6 +166,8 @@ String walletTypeToDisplayName(WalletType type) {
return 'Polygon (MATIC)'; return 'Polygon (MATIC)';
case WalletType.solana: case WalletType.solana:
return 'Solana (SOL)'; return 'Solana (SOL)';
case WalletType.tron:
return 'Tron (TRX)';
default: default:
return ''; return '';
} }
@ -183,6 +195,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) {
return CryptoCurrency.maticpoly; return CryptoCurrency.maticpoly;
case WalletType.solana: case WalletType.solana:
return CryptoCurrency.sol; return CryptoCurrency.sol;
case WalletType.tron:
return CryptoCurrency.trx;
default: default:
throw Exception( throw Exception(
'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency');

30
cw_tron/.gitignore vendored Normal file
View file

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

10
cw_tron/.metadata Normal file
View file

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

3
cw_tron/CHANGELOG.md Normal file
View file

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

1
cw_tron/LICENSE Normal file
View file

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

39
cw_tron/README.md Normal file
View file

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

View file

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

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

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

View file

@ -0,0 +1,103 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_tron/tron_token.dart';
class DefaultTronTokens {
final List<TronToken> _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<TronToken> 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();
}

39
cw_tron/lib/file.dart Normal file
View file

@ -0,0 +1,39 @@
import 'dart:io';
import 'package:cw_core/key.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
Future<void> 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<void> 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<String> 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);
}

View file

@ -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<int> 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<void> commit() async => await sendTransaction();
@override
String get feeFormatted => fee;
@override
String get hex => bytesToHex(signedTransaction);
@override
String get id => '';
}

436
cw_tron/lib/tron_abi.dart Normal file
View file

@ -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"
}
];

View file

@ -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);
}
}
}

View file

@ -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<List<TronTransactionModel>> 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<String, dynamic>;
if (response.statusCode >= 200 &&
response.statusCode < 300 &&
jsonResponse['status'] != false) {
return (jsonResponse['data'] as List).map((e) {
return TronTransactionModel.fromJson(e as Map<String, dynamic>);
}).toList();
}
return [];
} catch (e, s) {
log('Error getting tx: ${e.toString()}\n ${s.toString()}');
return [];
}
}
Future<List<TronTRC20TransactionModel>> 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<String, dynamic>;
if (response.statusCode >= 200 &&
response.statusCode < 300 &&
jsonResponse['status'] != false) {
return (jsonResponse['data'] as List).map((e) {
return TronTRC20TransactionModel.fromJson(e as Map<String, dynamic>);
}).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<BigInt> getBalance(TronAddress address) async {
try {
final accountDetails = await _provider!.request(TronRequestGetAccount(address: address));
return accountDetails?.balance ?? BigInt.zero;
} catch (_) {
return BigInt.zero;
}
}
Future<int> 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<int> 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<int> 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<PendingTronTransaction> 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<TransactionRaw> _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<TransactionRaw> _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<String> sendTransaction({
required TransactionRaw rawTransaction,
required List<int> 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<TronBalance> 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<TronToken?> 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<dynamic> 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;
}
}
}

View file

@ -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;
}

View file

@ -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<Map<String, dynamic>> 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<String, dynamic>;
return data;
}
@override
Future<Map<String, dynamic>> 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<String, dynamic>;
return data;
}
}

View file

@ -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;
}

View file

@ -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<OutputInfo> outputs;
final CryptoCurrency currency;
}

View file

@ -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<TronTransactionInfo>
with Store {
TronTransactionHistoryBase({required this.walletInfo, required String password})
: _password = password {
transactions = ObservableMap<String, TronTransactionInfo>();
}
String _password;
final WalletInfo walletInfo;
Future<void> init() async => await _load();
@override
Future<void> 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<String, TronTransactionInfo> transactions) =>
this.transactions.addAll(transactions);
Future<Map<String, dynamic>> _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<String, dynamic>;
}
Future<void> _load() async {
try {
final content = await _read();
final txs = content['transactions'] as Map<String, dynamic>? ?? {};
for (var entry in txs.entries) {
final val = entry.value;
if (val is Map<String, dynamic>) {
final tx = TronTransactionInfo.fromJson(val);
_update(tx);
}
}
} catch (e) {
log(e.toString());
}
}
void _update(TronTransactionInfo transaction) => transactions[transaction.id] = transaction;
}

View file

@ -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<String, dynamic> 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<String, dynamic> 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);
}

View file

@ -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<String, dynamic> 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>? ret;
String? txID;
int? blockTimestamp;
List<Contract>? 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<String, dynamic> json) {
if (json['ret'] != null) {
ret = <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<String, dynamic>))
.toList()
: null;
}
}
class Ret {
String? contractRet;
int? fee;
Ret({this.contractRet, this.fee});
Ret.fromJson(Map<String, dynamic> json) {
contractRet = json['contractRet'];
fee = json['fee'];
}
}
class Contract {
Parameter? parameter;
String? type;
Contract({this.parameter, this.type});
Contract.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}
}

View file

@ -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<TronBalance, TronTransactionHistory, TronTransactionInfo> 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<CryptoCurrency, TronBalance>.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<TronToken> 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<CryptoCurrency, TronBalance> balance;
Completer<SharedPreferences> sharedPrefs = Completer();
Future<void> 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<TronWallet> 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<void> initTronTokensBox() async {
final boxName = "${walletInfo.name.replaceAll(" ", "_")}_${TronToken.boxName}";
tronTokensBox = await CakeHive.openBox<TronToken>(boxName);
}
String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name';
Future<TronPrivateKey> 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<void> changePassword(String password) {
throw UnimplementedError("changePassword");
}
@override
void close() {
_transactionsUpdateTimer?.cancel();
}
@action
@override
Future<void> connectToNode({required Node node}) async {
try {
syncStatus = ConnectingSyncStatus();
final isConnected = _client.connect(node);
if (!isConnected) {
throw Exception("${walletInfo.type.name.toUpperCase()} Node connection failed");
}
_getEstimatedFees();
_setTransactionUpdateTimer();
syncStatus = ConnectedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
Future<void> _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<int> _getNativeTxFee() async {
try {
final fee = await _client.getEstimatedFee(_tronPublicKey.toAddress());
return fee;
} catch (e) {
log(e.toString());
return 0;
}
}
Future<int> _getTrc20TxFee() async {
try {
final trc20fee = await _client.getTRCEstimatedFee(_tronPublicKey.toAddress());
return trc20fee;
} catch (e) {
log(e.toString());
return 0;
}
}
@action
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
syncStatus = SyncedSyncStatus();
} catch (e) {
syncStatus = FailedSyncStatus();
}
}
@override
Future<PendingTransaction> 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<Map<String, TronTransactionInfo>> fetchTransactions() async {
final address = _tronAddress;
final transactions = await _client.fetchTransactions(address);
final Map<String, TronTransactionInfo> 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<void> fetchTrc20ExcludedTransactions() async {
final address = _tronAddress;
final transactions = await _client.fetchTrc20ExcludedTransactions(address);
final Map<String, TronTransactionInfo> 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<void> rescan({required int height}) {
throw UnimplementedError("rescan");
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();
final path = await makePath();
await write(path: path, password: _password, data: toJSON());
await transactionHistory.save();
}
@override
String? get seed => _mnemonic;
@override
String get privateKey => _tronPrivateKey.toHex();
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
String toJSON() => json.encode({
'mnemonic': _mnemonic,
'private_key': privateKey,
'balance': balance[currency]!.toJSON(),
});
Future<void> _updateBalance() async {
balance[currency] = await _fetchTronBalance();
await _fetchTronTokenBalances();
await save();
}
Future<TronBalance> _fetchTronBalance() async {
final balance = await _client.getBalance(_tronPublicKey.toAddress());
return TronBalance(balance);
}
Future<void> _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<void>? updateBalance() async => await _updateBalance();
List<TronToken> get tronTokenCurrencies => tronTokensBox.values.toList();
Future<void> 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<void> deleteTronToken(TronToken token) async {
await token.delete();
balance.remove(token);
await _removeTokenTransactionsInHistory(token);
_updateBalance();
}
Future<void> _removeTokenTransactionsInHistory(TronToken token) async {
transactionHistory.transactions.removeWhere((key, value) => value.tokenSymbol == token.title);
await transactionHistory.save();
}
Future<TronToken?> getTronToken(String contractAddress) async =>
await _client.getTronToken(contractAddress, _tronAddress);
@override
Future<void> 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();
}
}

View file

@ -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<void> init() async {
address = walletInfo.address;
await updateAddressesInBox();
}
@override
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
await saveAddressesInBox();
} catch (e) {
log(e.toString());
}
}
}

View file

@ -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;
}

View file

@ -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<TronNewWalletCredentials,
TronRestoreWalletFromSeedCredentials, TronRestoreWalletFromPrivateKey> {
TronWalletService(this.walletInfoSource, {required this.client});
late TronClient client;
final Box<WalletInfo> walletInfoSource;
@override
WalletType getType() => WalletType.tron;
@override
Future<TronWallet> 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<TronWallet> 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<TronWallet> 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<TronWallet> 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<void> 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<bool> isWalletExit(String name) async =>
File(await pathForWallet(name: name, type: getType())).existsSync();
@override
Future<void> remove(String wallet) async {
File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true);
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);
}
}

33
cw_tron/pubspec.yaml Normal file
View file

@ -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

View file

@ -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);
});
}

View file

@ -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 wont 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, itll 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. Heres a template for the function.
Future<List<Node>> loadDefaultWalletXNodes() async {
final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml');
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.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, well 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<Node> 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. Heres the format for this function
Future<void> addWalletXNodeList({required Box<Node> 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, well 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`, well use it to identify the current node id.
Future<void> changeWalletXCurrentNodeToDefault(
{required SharedPreferences sharedPreferences, required Box<Node> 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 its 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 weve 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, dont 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

View file

@ -190,6 +190,36 @@
<string>solana-wallet</string> <string>solana-wallet</string>
</array> </array>
</dict> </dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>tron</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tron</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>tron-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tron-wallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>tron_wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tron_wallet</string>
</array>
</dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>$(CURRENT_PROJECT_VERSION)</string>

View file

@ -294,6 +294,8 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'; '|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.sol: case CryptoCurrency.sol:
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)'; 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: default:
if (type.tag == CryptoCurrency.eth.title) { if (type.tag == CryptoCurrency.eth.title) {
return '0x[0-9a-zA-Z]{42}'; return '0x[0-9a-zA-Z]{42}';

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/nano/nano.dart';
@ -40,6 +41,8 @@ class SeedValidator extends Validator<MnemonicItem> {
return polygon!.getPolygonWordList(language); return polygon!.getPolygonWordList(language);
case WalletType.solana: case WalletType.solana:
return solana!.getSolanaWordList(language); return solana!.getSolanaWordList(language);
case WalletType.tron:
return tron!.getTronWordList(language);
default: default:
return []; return [];
} }

View file

@ -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/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.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:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
@ -873,6 +874,8 @@ Future<void> setup({
return polygon!.createPolygonWalletService(_walletInfoSource); return polygon!.createPolygonWalletService(_walletInfoSource);
case WalletType.solana: case WalletType.solana:
return solana!.createSolanaWalletService(_walletInfoSource); return solana!.createSolanaWalletService(_walletInfoSource);
case WalletType.tron:
return tron!.createTronWalletService(_walletInfoSource);
default: default:
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
} }

View file

@ -36,6 +36,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'rpc.ankr.com'; const solanaDefaultNodeUri = 'rpc.ankr.com';
const tronDefaultNodeUri = 'api.trongrid.io';
const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
Future<void> defaultSettingsMigration( Future<void> defaultSettingsMigration(
@ -207,23 +208,22 @@ Future<void> defaultSettingsMigration(
case 28: case 28:
await _updateMoneroPriority(sharedPreferences); await _updateMoneroPriority(sharedPreferences);
break; break;
case 29: case 29:
await changeDefaultBitcoinNode(nodes, sharedPreferences); await changeDefaultBitcoinNode(nodes, sharedPreferences);
break; break;
case 30: case 30:
await disableServiceStatusFiatDisabled(sharedPreferences); await disableServiceStatusFiatDisabled(sharedPreferences);
break; break;
case 31: case 31:
await updateNanoNodeList(nodes: nodes); await updateNanoNodeList(nodes: nodes);
break; break;
case 32: case 32:
await updateBtcNanoWalletInfos(walletInfoSource); await updateBtcNanoWalletInfos(walletInfoSource);
break; break;
case 33:
await addTronNodeList(nodes: nodes);
await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes);
break;
default: default:
break; break;
} }
@ -478,6 +478,11 @@ Node? getSolanaDefaultNode({required Box<Node> nodes}) {
nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana);
} }
Node? getTronDefaultNode({required Box<Node> nodes}) {
return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ??
nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron);
}
Future<void> insecureStorageMigration({ Future<void> insecureStorageMigration({
required SharedPreferences sharedPreferences, required SharedPreferences sharedPreferences,
required FlutterSecureStorage secureStorage, required FlutterSecureStorage secureStorage,
@ -809,6 +814,7 @@ Future<void> checkCurrentNodes(
final currentBitcoinCashNodeId = final currentBitcoinCashNodeId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey);
final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
final currentMoneroNode = final currentMoneroNode =
nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId);
final currentBitcoinElectrumServer = final currentBitcoinElectrumServer =
@ -829,6 +835,8 @@ Future<void> checkCurrentNodes(
nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId); nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId);
final currentSolanaNodeServer = final currentSolanaNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId); nodeSource.values.firstWhereOrNull((node) => node.key == currentSolanaNodeId);
final currentTronNodeServer =
nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId);
if (currentMoneroNode == null) { if (currentMoneroNode == null) {
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
await nodeSource.add(newCakeWalletNode); await nodeSource.add(newCakeWalletNode);
@ -894,6 +902,12 @@ Future<void> checkCurrentNodes(
await nodeSource.add(node); await nodeSource.add(node);
await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); 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<void> resetBitcoinElectrumServer( Future<void> resetBitcoinElectrumServer(
@ -1022,3 +1036,20 @@ Future<void> changeSolanaCurrentNodeToDefault(
await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId);
} }
Future<void> addTronNodeList({required Box<Node> 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<void> changeTronCurrentNodeToDefault(
{required SharedPreferences sharedPreferences, required Box<Node> nodes}) async {
final node = getTronDefaultNode(nodes: nodes);
final nodeId = node?.key as int? ?? 0;
await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId);
}

View file

@ -166,6 +166,23 @@ Future<List<Node>> loadDefaultSolanaNodes() async {
return nodes; return nodes;
} }
Future<List<Node>> loadDefaultTronNodes() async {
final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml');
final loadedNodes = loadYaml(nodesRaw) as YamlList;
final nodes = <Node>[];
for (final raw in loadedNodes) {
if (raw is Map) {
final node = Node.fromMap(Map<String, Object>.from(raw));
node.type = WalletType.tron;
nodes.add(node);
}
}
return nodes;
}
Future<void> resetToDefault(Box<Node> nodeSource) async { Future<void> resetToDefault(Box<Node> nodeSource) async {
final moneroNodes = await loadDefaultNodes(); final moneroNodes = await loadDefaultNodes();
final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList();
@ -176,6 +193,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
final nanoNodes = await loadDefaultNanoNodes(); final nanoNodes = await loadDefaultNanoNodes();
final polygonNodes = await loadDefaultPolygonNodes(); final polygonNodes = await loadDefaultPolygonNodes();
final solanaNodes = await loadDefaultSolanaNodes(); final solanaNodes = await loadDefaultSolanaNodes();
final tronNodes = await loadDefaultTronNodes();
final nodes = moneroNodes + final nodes = moneroNodes +
bitcoinElectrumServerList + bitcoinElectrumServerList +
@ -185,7 +203,7 @@ Future<void> resetToDefault(Box<Node> nodeSource) async {
bitcoinCashElectrumServerList + bitcoinCashElectrumServerList +
nanoNodes + nanoNodes +
polygonNodes + polygonNodes +
solanaNodes; solanaNodes + tronNodes;
await nodeSource.clear(); await nodeSource.clear();
await nodeSource.addAll(nodes); await nodeSource.addAll(nodes);

View file

@ -14,6 +14,7 @@ class PreferencesKey {
static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentFiatCurrencyKey = 'current_fiat_currency';
static const currentBitcoinCashNodeIdKey = 'current_node_id_bch'; static const currentBitcoinCashNodeIdKey = 'current_node_id_bch';
static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentSolanaNodeIdKey = 'current_node_id_sol';
static const currentTronNodeIdKey = 'current_node_id_trx';
static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority';
static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode';
static const shouldSaveRecipientAddressKey = 'save_recipient_address'; static const shouldSaveRecipientAddressKey = 'save_recipient_address';

View file

@ -23,10 +23,11 @@ List<TransactionPriority> priorityForWalletType(WalletType type) {
return bitcoinCash!.getTransactionPriorities(); return bitcoinCash!.getTransactionPriorities();
case WalletType.polygon: case WalletType.polygon:
return polygon!.getTransactionPriorities(); return polygon!.getTransactionPriorities();
// no such thing for nano/banano/solana: // no such thing for nano/banano/solana/tron:
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return []; return [];
default: default:
return []; return [];

View file

@ -69,6 +69,13 @@ class ProvidersHelper {
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay]; return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood, ProviderType.moonpay];
case WalletType.solana: case WalletType.solana:
return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood];
case WalletType.tron:
return [
ProviderType.askEachTime,
ProviderType.onramper,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.none: case WalletType.none:
case WalletType.haven: case WalletType.haven:
return []; return [];
@ -96,6 +103,12 @@ class ProvidersHelper {
ProviderType.robinhood, ProviderType.robinhood,
ProviderType.moonpay, ProviderType.moonpay,
]; ];
case WalletType.tron:
return [
ProviderType.askEachTime,
ProviderType.robinhood,
ProviderType.moonpay,
];
case WalletType.monero: case WalletType.monero:
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:

View file

@ -167,7 +167,7 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions, transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage, secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo, anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 32, initialMigrationVersion: 33,
); );
} }

View file

@ -8,6 +8,7 @@ import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_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/settings_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -53,6 +54,11 @@ Future<void> startFiatRateUpdate(
solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); 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) { if (currencies != null) {
for (final currency in currencies) { for (final currency in currencies) {

View file

@ -4,8 +4,8 @@ import 'package:cake_wallet/entities/update_haven_rate.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.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/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
@ -70,8 +70,10 @@ void startCurrentWalletChangeReaction(
.get<SharedPreferences>() .get<SharedPreferences>()
.setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type));
if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin || if (wallet.type == WalletType.monero ||
wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) { wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash) {
_setAutoGenerateSubaddressStatus(wallet, settingsStore); _setAutoGenerateSubaddressStatus(wallet, settingsStore);
} }
@ -124,6 +126,10 @@ void startCurrentWalletChangeReaction(
currencies = currencies =
solana!.getSPLTokenCurrencies(appStore.wallet!).where((element) => element.enabled); 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) { if (currencies != null) {
for (final currency in currencies) { for (final currency in currencies) {

View file

@ -38,6 +38,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final bananoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); final bananoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24);
final solanaIcon = Image.asset('assets/images/sol_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 nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset( Image _newWalletImage(BuildContext context) => Image.asset(
@ -156,6 +157,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return polygonIcon; return polygonIcon;
case WalletType.solana: case WalletType.solana:
return solanaIcon; return solanaIcon;
case WalletType.tron:
return tronIcon;
default: default:
return nonWalletTypeIcon; return nonWalletTypeIcon;
} }

View file

@ -82,21 +82,27 @@ class TransactionsPage extends StatelessWidget {
} }
if (item is TransactionListItem) { if (item is TransactionListItem) {
if (item.hasTokens && item.assetOfTransaction == null) {
return Container();
}
final transaction = item.transaction; final transaction = item.transaction;
return Observer( return Observer(
builder: (_) => TransactionRow( builder: (_) => TransactionRow(
onTap: () => Navigator.of(context) onTap: () => Navigator.of(context)
.pushNamed(Routes.transactionDetails, arguments: transaction), .pushNamed(Routes.transactionDetails, arguments: transaction),
direction: transaction.direction, direction: transaction.direction,
formattedDate: DateFormat('HH:mm').format(transaction.date), formattedDate: DateFormat('HH:mm').format(transaction.date),
formattedAmount: item.formattedCryptoAmount, formattedAmount: item.formattedCryptoAmount,
formattedFiatAmount: formattedFiatAmount:
dashboardViewModel.balanceViewModel.isFiatDisabled dashboardViewModel.balanceViewModel.isFiatDisabled
? '' ? ''
: item.formattedFiatAmount, : item.formattedFiatAmount,
isPending: transaction.isPending, isPending: transaction.isPending,
title: item.formattedTitle + item.formattedStatus)); title: item.formattedTitle + item.formattedStatus,
),
);
} }
if (item is AnonpayTransactionListItem) { if (item is AnonpayTransactionListItem) {

View file

@ -34,7 +34,8 @@ class MenuWidgetState extends State<MenuWidget> {
this.bananoIcon = Image.asset('assets/images/nano_icon.png'), this.bananoIcon = Image.asset('assets/images/nano_icon.png'),
this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'), this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'),
this.polygonIcon = Image.asset('assets/images/matic_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; final largeScreen = 731;
@ -57,6 +58,7 @@ class MenuWidgetState extends State<MenuWidget> {
Image bananoIcon; Image bananoIcon;
Image polygonIcon; Image polygonIcon;
Image solanaIcon; Image solanaIcon;
Image tronIcon;
@override @override
void initState() { void initState() {
@ -226,6 +228,8 @@ class MenuWidgetState extends State<MenuWidget> {
return polygonIcon; return polygonIcon;
case WalletType.solana: case WalletType.solana:
return solanaIcon; return solanaIcon;
case WalletType.tron:
return tronIcon;
default: default:
throw Exception('No icon for ${type.toString()}'); throw Exception('No icon for ${type.toString()}');
} }

View file

@ -104,6 +104,7 @@ class WalletListBodyState extends State<WalletListBody> {
final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); 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 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 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 scrollController = ScrollController();
final double tileHeight = 60; final double tileHeight = 60;
Flushbar<void>? _progressBar; Flushbar<void>? _progressBar;
@ -316,6 +317,8 @@ class WalletListBodyState extends State<WalletListBody> {
return polygonIcon; return polygonIcon;
case WalletType.solana: case WalletType.solana:
return solanaIcon; return solanaIcon;
case WalletType.tron:
return tronIcon;
default: default:
return nonWalletTypeIcon; return nonWalletTypeIcon;
} }

View file

@ -872,6 +872,7 @@ abstract class SettingsStoreBase with Store {
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
final moneroNode = nodeSource.get(nodeId); final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@ -882,6 +883,7 @@ abstract class SettingsStoreBase with Store {
final nanoNode = nodeSource.get(nanoNodeId); final nanoNode = nodeSource.get(nanoNodeId);
final nanoPowNode = powNodeSource.get(nanoPowNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId);
final solanaNode = nodeSource.get(solanaNodeId); final solanaNode = nodeSource.get(solanaNodeId);
final tronNode = nodeSource.get(tronNodeId);
final packageInfo = await PackageInfo.fromPlatform(); final packageInfo = await PackageInfo.fromPlatform();
final deviceName = await _getDeviceName() ?? ''; final deviceName = await _getDeviceName() ?? '';
final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true;
@ -944,6 +946,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.solana] = solanaNode; nodes[WalletType.solana] = solanaNode;
} }
if (tronNode != null) {
nodes[WalletType.tron] = tronNode;
}
final savedSyncMode = SyncMode.all.firstWhere((element) { final savedSyncMode = SyncMode.all.firstWhere((element) {
return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); 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 polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey);
final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey);
final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); final solanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey);
final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey);
final moneroNode = nodeSource.get(nodeId); final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
@ -1247,6 +1254,7 @@ abstract class SettingsStoreBase with Store {
final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId);
final nanoNode = nodeSource.get(nanoNodeId); final nanoNode = nodeSource.get(nanoNodeId);
final solanaNode = nodeSource.get(solanaNodeId); final solanaNode = nodeSource.get(solanaNodeId);
final tronNode = nodeSource.get(tronNodeId);
if (moneroNode != null) { if (moneroNode != null) {
nodes[WalletType.monero] = moneroNode; nodes[WalletType.monero] = moneroNode;
} }
@ -1283,6 +1291,10 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.solana] = solanaNode; nodes[WalletType.solana] = solanaNode;
} }
if (tronNode != null) {
nodes[WalletType.tron] = tronNode;
}
// MIGRATED: // MIGRATED:
useTOTP2FA = await SecureKey.getBool( useTOTP2FA = await SecureKey.getBool(
@ -1413,6 +1425,9 @@ abstract class SettingsStoreBase with Store {
case WalletType.solana: case WalletType.solana:
await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int); await _sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, node.key as int);
break; break;
case WalletType.tron:
await _sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, node.key as int);
break;
default: default:
break; break;
} }

114
lib/tron/cw_tron.dart Normal file
View file

@ -0,0 +1,114 @@
part of 'tron.dart';
class CWTron extends Tron {
@override
List<String> getTronWordList(String language) => EVMChainMnemonics.englishWordlist;
WalletService createTronWalletService(Box<WalletInfo> 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<Output> 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<TronToken> getTronTokenCurrencies(WalletBase wallet) =>
(wallet as TronWallet).tronTokenCurrencies;
@override
Future<void> 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<void> deleteTronToken(WalletBase wallet, CryptoCurrency token) async =>
await (wallet as TronWallet).deleteTronToken(token as TronToken);
@override
Future<TronToken?> 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;
}

View file

@ -38,6 +38,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
case WalletType.polygon: case WalletType.polygon:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return true; return true;
case WalletType.monero: case WalletType.monero:
case WalletType.none: case WalletType.none:

View file

@ -80,7 +80,9 @@ abstract class BalanceViewModelBase with Store {
@computed @computed
bool get isHomeScreenSettingsEnabled => bool get isHomeScreenSettingsEnabled =>
isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; isEVMCompatibleChain(wallet.type) ||
wallet.type == WalletType.solana ||
wallet.type == WalletType.tron;
@computed @computed
bool get hasAccounts => wallet.type == WalletType.monero; bool get hasAccounts => wallet.type == WalletType.monero;
@ -126,6 +128,7 @@ abstract class BalanceViewModelBase with Store {
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return S.current.xmr_available_balance; return S.current.xmr_available_balance;
default: default:
return S.current.confirmed; return S.current.confirmed;
@ -140,6 +143,7 @@ abstract class BalanceViewModelBase with Store {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return S.current.xmr_full_balance; return S.current.xmr_full_balance;
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
@ -287,6 +291,7 @@ abstract class BalanceViewModelBase with Store {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
return false; return false;
default: default:
return true; return true;

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/settings_store.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:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/erc20_token.dart';
@ -79,6 +80,10 @@ abstract class HomeSettingsViewModelBase with Store {
); );
} }
if (_balanceViewModel.wallet.type == WalletType.tron) {
await tron!.addTronToken(_balanceViewModel.wallet, token, contractAddress);
}
_updateTokensList(); _updateTokensList();
_updateFiatPrices(token); _updateFiatPrices(token);
} }
@ -96,6 +101,9 @@ abstract class HomeSettingsViewModelBase with Store {
await solana!.deleteSPLToken(_balanceViewModel.wallet, token); await solana!.deleteSPLToken(_balanceViewModel.wallet, token);
} }
if (_balanceViewModel.wallet.type == WalletType.tron) {
await tron!.deleteTronToken(_balanceViewModel.wallet, token);
}
_updateTokensList(); _updateTokensList();
} }
@ -112,6 +120,10 @@ abstract class HomeSettingsViewModelBase with Store {
return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress); return await solana!.getSPLToken(_balanceViewModel.wallet, contractAddress);
} }
if (_balanceViewModel.wallet.type == WalletType.tron) {
return await tron!.getTronToken(_balanceViewModel.wallet, contractAddress);
}
return null; return null;
} }
@ -143,6 +155,11 @@ abstract class HomeSettingsViewModelBase with Store {
solana!.addSPLToken(_balanceViewModel.wallet, token, address); solana!.addSPLToken(_balanceViewModel.wallet, token, address);
} }
if (_balanceViewModel.wallet.type == WalletType.tron) {
final address = tron!.getTokenAddress(token);
tron!.addTronToken(_balanceViewModel.wallet, token, address);
}
_refreshTokensList(); _refreshTokensList();
} }
@ -189,6 +206,14 @@ abstract class HomeSettingsViewModelBase with Store {
.toList() .toList()
..sort(_sortFunc)); ..sort(_sortFunc));
} }
if (_balanceViewModel.wallet.type == WalletType.tron) {
tokens.addAll(tron!
.getTronTokenCurrencies(_balanceViewModel.wallet)
.where((element) => _matchesSearchText(element))
.toList()
..sort(_sortFunc));
}
} }
@action @action
@ -207,7 +232,7 @@ abstract class HomeSettingsViewModelBase with Store {
bool _matchesSearchText(CryptoCurrency asset) { bool _matchesSearchText(CryptoCurrency asset) {
final address = getTokenAddressBasedOnWallet(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; if (address == null) return false;
return searchText.isEmpty || return searchText.isEmpty ||
@ -217,6 +242,10 @@ abstract class HomeSettingsViewModelBase with Store {
} }
String? getTokenAddressBasedOnWallet(CryptoCurrency asset) { String? getTokenAddressBasedOnWallet(CryptoCurrency asset) {
if (_balanceViewModel.wallet.type == WalletType.tron) {
return tron!.getTokenAddress(asset);
}
if (_balanceViewModel.wallet.type == WalletType.solana) { if (_balanceViewModel.wallet.type == WalletType.solana) {
return solana!.getTokenAddress(asset); return solana!.getTokenAddress(asset);
} }
@ -229,7 +258,7 @@ abstract class HomeSettingsViewModelBase with Store {
return polygon!.getTokenAddress(asset); 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; return null;
} }
} }

View file

@ -4,7 +4,10 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/polygon/polygon.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/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_direction.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
@ -34,6 +37,11 @@ class TransactionListItem extends ActionListItem with Keyable {
@override @override
dynamic get keyIndex => transaction.id; 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 { String get formattedCryptoAmount {
return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted(); return displayMode == BalanceDisplayMode.hiddenBalance ? '---' : transaction.amountFormatted();
} }
@ -63,6 +71,34 @@ class TransactionListItem extends ActionListItem with Keyable {
return transaction.isPending ? S.current.pending : ''; 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 { String get formattedFiatAmount {
var amount = ''; var amount = '';
@ -114,6 +150,16 @@ class TransactionListItem extends ActionListItem with Keyable {
price: price, price: price,
); );
break; 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: default:
break; break;
} }

View file

@ -178,6 +178,10 @@ abstract class ExchangeTradeViewModelBase with Store {
wallet.currency == CryptoCurrency.maticpoly && wallet.currency == CryptoCurrency.maticpoly &&
tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag; tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag;
bool _isTronToken() =>
wallet.currency == CryptoCurrency.trx &&
tradesStore.trade!.from.tag == CryptoCurrency.trx.title;
bool _isSplToken() => bool _isSplToken() =>
wallet.currency == CryptoCurrency.sol && wallet.currency == CryptoCurrency.sol &&
tradesStore.trade!.from.tag == CryptoCurrency.sol.title; tradesStore.trade!.from.tag == CryptoCurrency.sol.title;
@ -186,6 +190,7 @@ abstract class ExchangeTradeViewModelBase with Store {
tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
_isEthToken() || _isEthToken() ||
_isPolygonToken() || _isPolygonToken() ||
_isSplToken(); _isSplToken() ||
_isTronToken();
} }
} }

View file

@ -676,6 +676,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
depositCurrency = CryptoCurrency.sol; depositCurrency = CryptoCurrency.sol;
receiveCurrency = CryptoCurrency.xmr; receiveCurrency = CryptoCurrency.xmr;
break; break;
case WalletType.tron:
depositCurrency = CryptoCurrency.trx;
receiveCurrency = CryptoCurrency.xmr;
break;
default: default:
break; break;
} }

View file

@ -76,6 +76,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
case WalletType.solana: case WalletType.solana:
case WalletType.banano: case WalletType.banano:
case WalletType.nano: case WalletType.nano:
case WalletType.tron:
return true; return true;
case WalletType.none: case WalletType.none:
case WalletType.monero: case WalletType.monero:

View file

@ -82,6 +82,9 @@ abstract class NodeListViewModelBase with Store {
case WalletType.solana: case WalletType.solana:
node = getSolanaDefaultNode(nodes: _nodeSource)!; node = getSolanaDefaultNode(nodes: _nodeSource)!;
break; break;
case WalletType.tron:
node = getTronDefaultNode(nodes: _nodeSource)!;
break;
default: default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}');
} }

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.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_mode.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -86,6 +87,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
case WalletType.solana: case WalletType.solana:
return solana!.createSolanaRestoreWalletFromPrivateKey( return solana!.createSolanaRestoreWalletFromPrivateKey(
name: name, password: password, privateKey: restoreWallet.privateKey!); name: name, password: password, privateKey: restoreWallet.privateKey!);
case WalletType.tron:
return tron!.createTronRestoreWalletFromPrivateKey(
name: name, password: password, privateKey: restoreWallet.privateKey!);
default: default:
throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); throw Exception('Unexpected type: ${restoreWallet.type.toString()}');
} }
@ -130,6 +134,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
case WalletType.solana: case WalletType.solana:
return solana!.createSolanaRestoreWalletFromSeedCredentials( return solana!.createSolanaRestoreWalletFromSeedCredentials(
name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
case WalletType.tron:
return tron!.createTronRestoreWalletFromSeedCredentials(
name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password);
default: default:
throw Exception('Unexpected type: ${type.toString()}'); throw Exception('Unexpected type: ${type.toString()}');
} }

View file

@ -33,6 +33,9 @@ class WalletRestoreFromQRCode {
'bitcoincash-wallet': WalletType.bitcoinCash, 'bitcoincash-wallet': WalletType.bitcoinCash,
'bitcoincash_wallet': WalletType.bitcoinCash, 'bitcoincash_wallet': WalletType.bitcoinCash,
'solana-wallet': WalletType.solana, 'solana-wallet': WalletType.solana,
'tron': WalletType.tron,
'tron-wallet': WalletType.tron,
'tron_wallet': WalletType.tron,
}; };
static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null;
@ -184,6 +187,14 @@ class WalletRestoreFromQRCode {
return WalletRestoreMode.keys; 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'); throw Exception('Unexpected restore mode: restore params are invalid');
} }
} }

View file

@ -8,6 +8,7 @@ import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.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/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:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@ -117,6 +118,17 @@ abstract class OutputBase with Store {
@computed @computed
double get estimatedFee { double get estimatedFee {
try { 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) { if (_wallet.type == WalletType.solana) {
return solana!.getEstimateFees(_wallet) ?? 0.0; return solana!.getEstimateFees(_wallet) ?? 0.0;
} }
@ -163,8 +175,11 @@ abstract class OutputBase with Store {
@computed @computed
String get estimatedFeeFiatAmount { String get estimatedFeeFiatAmount {
try { try {
final currency = final currency = (isEVMCompatibleChain(_wallet.type) ||
isEVMCompatibleChain(_wallet.type) ? _wallet.currency : cryptoCurrencyHandler(); _wallet.type == WalletType.solana ||
_wallet.type == WalletType.tron)
? _wallet.currency
: cryptoCurrencyHandler();
final fiat = calculateFiatAmountRaw( final fiat = calculateFiatAmountRaw(
price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee);
return fiat; return fiat;
@ -269,6 +284,9 @@ abstract class OutputBase with Store {
case WalletType.solana: case WalletType.solana:
maximumFractionDigits = 12; maximumFractionDigits = 12;
break; break;
case WalletType.tron:
maximumFractionDigits = 12;
break;
default: default:
break; break;
} }

View file

@ -53,7 +53,8 @@ abstract class SendTemplateViewModelBase with Store {
_wallet.type != WalletType.haven && _wallet.type != WalletType.haven &&
_wallet.type != WalletType.ethereum && _wallet.type != WalletType.ethereum &&
_wallet.type != WalletType.polygon && _wallet.type != WalletType.polygon &&
_wallet.type != WalletType.solana; _wallet.type != WalletType.solana &&
_wallet.type != WalletType.tron;
@computed @computed
CryptoCurrency get cryptoCurrency => _wallet.currency; CryptoCurrency get cryptoCurrency => _wallet.currency;

View file

@ -12,6 +12,7 @@ import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.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/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cw_core/exceptions.dart'; import 'package:cw_core/exceptions.dart';
@ -50,7 +51,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
void onWalletChange(wallet) { void onWalletChange(wallet) {
currencies = wallet.balance.keys.toList(); currencies = wallet.balance.keys.toList();
selectedCryptoCurrency = wallet.currency; selectedCryptoCurrency = wallet.currency;
hasMultipleTokens = isEVMCompatibleChain(wallet.type) || wallet.type == WalletType.solana; hasMultipleTokens = isEVMCompatibleChain(wallet.type) ||
wallet.type == WalletType.solana ||
wallet.type == WalletType.tron;
} }
SendViewModelBase( SendViewModelBase(
@ -64,7 +67,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
currencies = appStore.wallet!.balance.keys.toList(), currencies = appStore.wallet!.balance.keys.toList(),
selectedCryptoCurrency = appStore.wallet!.currency, selectedCryptoCurrency = appStore.wallet!.currency,
hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) || hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
appStore.wallet!.type == WalletType.solana, appStore.wallet!.type == WalletType.solana ||
appStore.wallet!.type == WalletType.tron,
outputs = ObservableList<Output>(), outputs = ObservableList<Output>(),
_settingsStore = appStore.settingsStore, _settingsStore = appStore.settingsStore,
fiatFromSettings = appStore.settingsStore.fiatCurrency, fiatFromSettings = appStore.settingsStore.fiatCurrency,
@ -110,6 +114,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
@computed @computed
bool get isBatchSending => outputs.length > 1; bool get isBatchSending => outputs.length > 1;
bool get shouldDisplaySendALL => walletType != WalletType.solana || walletType != WalletType.tron;
@computed @computed
String get pendingTransactionFiatAmount { String get pendingTransactionFiatAmount {
if (pendingTransaction == null) { if (pendingTransaction == null) {
@ -236,7 +242,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool get hasFeesPriority => bool get hasFeesPriority =>
wallet.type != WalletType.nano && wallet.type != WalletType.nano &&
wallet.type != WalletType.banano && wallet.type != WalletType.banano &&
wallet.type != WalletType.solana; wallet.type != WalletType.solana &&
wallet.type != WalletType.tron;
@observable @observable
CryptoCurrency selectedCryptoCurrency; CryptoCurrency selectedCryptoCurrency;
@ -423,7 +431,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
Object _credentials() { Object _credentials() {
final priority = _settingsStore.priority[wallet.type]; 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}'); throw Exception('Priority is null for wallet type: ${wallet.type}');
} }
@ -453,6 +462,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.solana: case WalletType.solana:
return solana! return solana!
.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency); .createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
case WalletType.tron:
return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
default: default:
throw Exception('Unexpected wallet type: ${wallet.type}'); throw Exception('Unexpected wallet type: ${wallet.type}');
} }

View file

@ -56,8 +56,9 @@ abstract class OtherSettingsViewModelBase with Store {
_wallet.type == WalletType.nano || _wallet.type == WalletType.banano; _wallet.type == WalletType.nano || _wallet.type == WalletType.banano;
@computed @computed
bool get displayTransactionPriority => bool get displayTransactionPriority => !(changeRepresentativeEnabled ||
!(changeRepresentativeEnabled || _wallet.type == WalletType.solana); _wallet.type == WalletType.solana ||
_wallet.type == WalletType.tron);
@computed @computed
bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven; bool get isEnabledBuyAction => !_settingsStore.disableBuy && _wallet.type != WalletType.haven;

View file

@ -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/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.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:cake_wallet/view_model/send/send_view_model.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_priority.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:hive/hive.dart';
import 'package:intl/src/intl/date_format.dart'; import 'package:intl/src/intl/date_format.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -71,6 +72,9 @@ abstract class TransactionDetailsViewModelBase with Store {
case WalletType.solana: case WalletType.solana:
_addSolanaListItems(tx, dateFormat); _addSolanaListItems(tx, dateFormat);
break; break;
case WalletType.tron:
_addTronListItems(tx, dateFormat);
break;
default: default:
break; break;
} }
@ -160,6 +164,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return 'https://polygonscan.com/tx/${txId}'; return 'https://polygonscan.com/tx/${txId}';
case WalletType.solana: case WalletType.solana:
return 'https://solscan.io/tx/${txId}'; return 'https://solscan.io/tx/${txId}';
case WalletType.tron:
return 'https://tronscan.org/#/transaction/${txId}';
default: default:
return ''; return '';
} }
@ -186,6 +192,8 @@ abstract class TransactionDetailsViewModelBase with Store {
return S.current.view_transaction_on + 'polygonscan.com'; return S.current.view_transaction_on + 'polygonscan.com';
case WalletType.solana: case WalletType.solana:
return S.current.view_transaction_on + 'solscan.io'; return S.current.view_transaction_on + 'solscan.io';
case WalletType.tron:
return S.current.view_transaction_on + 'tronscan.org';
default: default:
return ''; return '';
} }
@ -339,20 +347,19 @@ abstract class TransactionDetailsViewModelBase with Store {
transactionInfo.inputAddresses?.length ?? 1, transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1); transactionInfo.outputAddresses?.length ?? 1);
RBFListItems.add(StandartListItem( RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
title: S.current.old_fee,
value: tx.feeFormatted() ?? '0.0'));
final priorities = priorityForWalletType(wallet.type); final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.transactionPriority); final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
final customItem = priorities.firstWhereOrNull( final customItem = priorities
(element) => element == sendViewModel.bitcoinTransactionPriorityCustom); .firstWhereOrNull((element) => element == sendViewModel.bitcoinTransactionPriorityCustom);
final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null; final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble(); final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble();
RBFListItems.add(StandardPickerListItem( RBFListItems.add(StandardPickerListItem(
title: S.current.estimated_new_fee, 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), items: priorityForWalletType(wallet.type),
customValue: settingsStore.customBitcoinFeeRate.toDouble(), customValue: settingsStore.customBitcoinFeeRate.toDouble(),
maxValue: maxCustomFeeRate, 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 @action
Future<void> _checkForRBF() async { Future<void> _checkForRBF() async {
if (wallet.type == WalletType.bitcoin && if (wallet.type == WalletType.bitcoin &&
@ -392,10 +420,10 @@ abstract class TransactionDetailsViewModelBase with Store {
newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount) ? bitcoin!.getEstimatedFeeWithFeeRate(wallet, value.round(), transactionInfo.amount)
: bitcoin!.getFeeAmountForPriority( : bitcoin!.getFeeAmountForPriority(
wallet, wallet,
priority, priority,
transactionInfo.inputAddresses?.length ?? 1, transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1); transactionInfo.outputAddresses?.length ?? 1);
return bitcoin!.formatterBitcoinAmountToString(amount: newFee); return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
} }

View file

@ -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/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/yat/yat_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/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_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_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 { abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
WalletAddressListViewModelBase({ WalletAddressListViewModelBase({
required AppStore appStore, required AppStore appStore,
@ -273,6 +289,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
return SolanaURI(amount: amount, address: address.address); 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()}'); throw Exception('Unexpected type: ${type.toString()}');
} }
@ -348,6 +368,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); 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) { if (searchText.isNotEmpty) {
return ObservableList.of(addressList.where((item) { return ObservableList.of(addressList.where((item) {
if (item is WalletAddressListItem) { if (item is WalletAddressListItem) {

View file

@ -118,7 +118,8 @@ abstract class WalletKeysViewModelBase with Store {
} }
if (isEVMCompatibleChain(_appStore.wallet!.type) || if (isEVMCompatibleChain(_appStore.wallet!.type) ||
_appStore.wallet!.type == WalletType.solana) { _appStore.wallet!.type == WalletType.solana ||
_appStore.wallet!.type == WalletType.tron) {
items.addAll([ items.addAll([
if (_appStore.wallet!.privateKey != null) if (_appStore.wallet!.privateKey != null)
StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!),
@ -175,6 +176,8 @@ abstract class WalletKeysViewModelBase with Store {
return 'polygon-wallet'; return 'polygon-wallet';
case WalletType.solana: case WalletType.solana:
return 'solana-wallet'; return 'solana-wallet';
case WalletType.tron:
return 'tron-wallet';
default: default:
throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}');
} }

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
@ -43,6 +44,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
return 16; return 16;
} }
return 25; return 25;
case WalletType.tron:
case WalletType.solana: case WalletType.solana:
case WalletType.polygon: case WalletType.polygon:
case WalletType.ethereum: case WalletType.ethereum:
@ -79,6 +81,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
return polygon!.createPolygonNewWalletCredentials(name: name); return polygon!.createPolygonNewWalletCredentials(name: name);
case WalletType.solana: case WalletType.solana:
return solana!.createSolanaNewWalletCredentials(name: name); return solana!.createSolanaNewWalletCredentials(name: name);
case WalletType.tron:
return tron!.createTronNewWalletCredentials(name: name);
default: default:
throw Exception('Unexpected type: ${type.toString()}'); throw Exception('Unexpected type: ${type.toString()}');
} }

View file

@ -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/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.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.polygon ||
type == WalletType.nano || type == WalletType.nano ||
type == WalletType.banano || type == WalletType.banano ||
type == WalletType.solana, type == WalletType.solana ||
type == WalletType.tron,
isButtonEnabled = false, isButtonEnabled = false,
mode = WalletRestoreMode.seed, mode = WalletRestoreMode.seed,
super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) {
@ -48,6 +50,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.nano: case WalletType.nano:
case WalletType.banano: case WalletType.banano:
case WalletType.solana: case WalletType.solana:
case WalletType.tron:
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
break; break;
default: default:
@ -127,6 +130,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
mnemonic: seed, mnemonic: seed,
password: password, password: password,
); );
case WalletType.tron:
return tron!.createTronRestoreWalletFromSeedCredentials(
name: name,
mnemonic: seed,
password: password,
);
default: default:
break; break;
} }
@ -185,6 +194,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
password: password, password: password,
privateKey: options['private_key'] as String, privateKey: options['private_key'] as String,
); );
case WalletType.tron:
return tron!.createTronRestoreWalletFromPrivateKey(
name: name,
password: password,
privateKey: options['private_key'] as String,
);
default: default:
break; break;
} }

View file

@ -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_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_solana && 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_ethereum && flutter pub get && cd ..
cd cw_polygon && flutter pub get && cd .. cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -158,6 +158,7 @@ flutter:
- assets/nano_pow_node_list.yml - assets/nano_pow_node_list.yml
- assets/polygon_node_list.yml - assets/polygon_node_list.yml
- assets/solana_node_list.yml - assets/solana_node_list.yml
- assets/tron_node_list.yml
- assets/text/ - assets/text/
- assets/faq/ - assets/faq/
- assets/animation/ - assets/animation/

View file

@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in
CONFIG_ARGS="--monero" CONFIG_ARGS="--monero"
;; ;;
$CAKEWALLET) $CAKEWALLET)
CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron"
;; ;;
$HAVEN) $HAVEN)
CONFIG_ARGS="--haven" CONFIG_ARGS="--haven"

View file

@ -28,7 +28,7 @@ case $APP_IOS_TYPE in
CONFIG_ARGS="--monero" CONFIG_ARGS="--monero"
;; ;;
$CAKEWALLET) $CAKEWALLET)
CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana" CONFIG_ARGS="--monero --bitcoin --haven --ethereum --polygon --nano --bitcoinCash --solana --tron"
;; ;;
$HAVEN) $HAVEN)

View file

@ -31,7 +31,7 @@ case $APP_MACOS_TYPE in
$MONERO_COM) $MONERO_COM)
CONFIG_ARGS="--monero";; CONFIG_ARGS="--monero";;
$CAKEWALLET) $CAKEWALLET)
CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana";; #--haven CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron";; #--haven
esac esac
cp -rf pubspec_description.yaml pubspec.yaml cp -rf pubspec_description.yaml pubspec.yaml

View file

@ -8,6 +8,7 @@ const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart';
const nanoOutputPath = 'lib/nano/nano.dart'; const nanoOutputPath = 'lib/nano/nano.dart';
const polygonOutputPath = 'lib/polygon/polygon.dart'; const polygonOutputPath = 'lib/polygon/polygon.dart';
const solanaOutputPath = 'lib/solana/solana.dart'; const solanaOutputPath = 'lib/solana/solana.dart';
const tronOutputPath = 'lib/tron/tron.dart';
const walletTypesPath = 'lib/wallet_types.g.dart'; const walletTypesPath = 'lib/wallet_types.g.dart';
const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecDefaultPath = 'pubspec_default.yaml';
const pubspecOutputPath = 'pubspec.yaml'; const pubspecOutputPath = 'pubspec.yaml';
@ -23,6 +24,7 @@ Future<void> main(List<String> args) async {
final hasBanano = args.contains('${prefix}banano'); final hasBanano = args.contains('${prefix}banano');
final hasPolygon = args.contains('${prefix}polygon'); final hasPolygon = args.contains('${prefix}polygon');
final hasSolana = args.contains('${prefix}solana'); final hasSolana = args.contains('${prefix}solana');
final hasTron = args.contains('${prefix}tron');
await generateBitcoin(hasBitcoin); await generateBitcoin(hasBitcoin);
await generateMonero(hasMonero); await generateMonero(hasMonero);
@ -32,6 +34,7 @@ Future<void> main(List<String> args) async {
await generateNano(hasNano); await generateNano(hasNano);
await generatePolygon(hasPolygon); await generatePolygon(hasPolygon);
await generateSolana(hasSolana); await generateSolana(hasSolana);
await generateTron(hasTron);
// await generateBanano(hasEthereum); // await generateBanano(hasEthereum);
await generatePubspec( await generatePubspec(
@ -44,6 +47,7 @@ Future<void> main(List<String> args) async {
hasBitcoinCash: hasBitcoinCash, hasBitcoinCash: hasBitcoinCash,
hasPolygon: hasPolygon, hasPolygon: hasPolygon,
hasSolana: hasSolana, hasSolana: hasSolana,
hasTron: hasTron,
); );
await generateWalletTypes( await generateWalletTypes(
hasMonero: hasMonero, hasMonero: hasMonero,
@ -55,6 +59,7 @@ Future<void> main(List<String> args) async {
hasBitcoinCash: hasBitcoinCash, hasBitcoinCash: hasBitcoinCash,
hasPolygon: hasPolygon, hasPolygon: hasPolygon,
hasSolana: hasSolana, hasSolana: hasSolana,
hasTron: hasTron,
); );
} }
@ -1024,6 +1029,79 @@ abstract class Solana {
await outputFile.writeAsString(output); await outputFile.writeAsString(output);
} }
Future<void> 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<String> getTronWordList(String language);
WalletService createTronWalletService(Box<WalletInfo> 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<Output> outputs, {
required CryptoCurrency currency,
});
List<CryptoCurrency> getTronTokenCurrencies(WalletBase wallet);
Future<void> addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress);
Future<void> deleteTronToken(WalletBase wallet, CryptoCurrency token);
Future<CryptoCurrency?> 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<void> generatePubspec( Future<void> generatePubspec(
{required bool hasMonero, {required bool hasMonero,
required bool hasBitcoin, required bool hasBitcoin,
@ -1033,7 +1111,8 @@ Future<void> generatePubspec(
required bool hasBanano, required bool hasBanano,
required bool hasBitcoinCash, required bool hasBitcoinCash,
required bool hasPolygon, required bool hasPolygon,
required bool hasSolana}) async { required bool hasSolana,
required bool hasTron}) async {
const cwCore = """ const cwCore = """
cw_core: cw_core:
path: ./cw_core path: ./cw_core
@ -1082,6 +1161,10 @@ Future<void> generatePubspec(
cw_evm: cw_evm:
path: ./cw_evm path: ./cw_evm
"""; """;
const cwTron = """
cw_tron:
path: ./cw_tron
""";
final inputFile = File(pubspecOutputPath); final inputFile = File(pubspecOutputPath);
final inputText = await inputFile.readAsString(); final inputText = await inputFile.readAsString();
final inputLines = inputText.split('\n'); final inputLines = inputText.split('\n');
@ -1121,6 +1204,10 @@ Future<void> generatePubspec(
output += '\n$cwSolana'; output += '\n$cwSolana';
} }
if (hasTron) {
output += '\n$cwTron';
}
if (hasHaven && !hasMonero) { if (hasHaven && !hasMonero) {
output += '\n$cwSharedExternal\n$cwHaven'; output += '\n$cwSharedExternal\n$cwHaven';
} else if (hasHaven) { } else if (hasHaven) {
@ -1152,7 +1239,8 @@ Future<void> generateWalletTypes(
required bool hasBanano, required bool hasBanano,
required bool hasBitcoinCash, required bool hasBitcoinCash,
required bool hasPolygon, required bool hasPolygon,
required bool hasSolana}) async { required bool hasSolana,
required bool hasTron}) async {
final walletTypesFile = File(walletTypesPath); final walletTypesFile = File(walletTypesPath);
if (walletTypesFile.existsSync()) { if (walletTypesFile.existsSync()) {
@ -1191,6 +1279,10 @@ Future<void> generateWalletTypes(
outputContent += '\tWalletType.solana,\n'; outputContent += '\tWalletType.solana,\n';
} }
if (hasTron) {
outputContent += '\tWalletType.tron,\n';
}
if (hasNano) { if (hasNano) {
outputContent += '\tWalletType.nano,\n'; outputContent += '\tWalletType.nano,\n';
} }

View file

@ -6,6 +6,7 @@ import 'utils/utils.dart';
const configPath = 'tool/.secrets-config.json'; const configPath = 'tool/.secrets-config.json';
const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; const evmChainsConfigPath = 'tool/.evm-secrets-config.json';
const solanaConfigPath = 'tool/.solana-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json';
const tronConfigPath = 'tool/.tron-secrets-config.json';
Future<void> main(List<String> args) async => generateSecretsConfig(args); Future<void> main(List<String> args) async => generateSecretsConfig(args);
@ -20,6 +21,7 @@ Future<void> generateSecretsConfig(List<String> args) async {
final configFile = File(configPath); final configFile = File(configPath);
final evmChainsConfigFile = File(evmChainsConfigPath); final evmChainsConfigFile = File(evmChainsConfigPath);
final solanaConfigFile = File(solanaConfigPath); final solanaConfigFile = File(solanaConfigPath);
final tronConfigFile = File(tronConfigPath);
final secrets = <String, dynamic>{}; final secrets = <String, dynamic>{};
@ -78,4 +80,18 @@ Future<void> generateSecretsConfig(List<String> args) async {
secretsJson = JsonEncoder.withIndent(' ').convert(secrets); secretsJson = JsonEncoder.withIndent(' ').convert(secrets);
await solanaConfigFile.writeAsString(secretsJson); 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);
} }

View file

@ -10,6 +10,9 @@ const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart';
const solanaConfigPath = 'tool/.solana-secrets-config.json'; const solanaConfigPath = 'tool/.solana-secrets-config.json';
const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart'; const solanaOutputPath = 'cw_solana/lib/.secrets.g.dart';
const tronConfigPath = 'tool/.tron-secrets-config.json';
const tronOutputPath = 'cw_tron/lib/.secrets.g.dart';
Future<void> main(List<String> args) async => importSecretsConfig(); Future<void> main(List<String> args) async => importSecretsConfig();
Future<void> importSecretsConfig() async { Future<void> importSecretsConfig() async {
@ -29,6 +32,11 @@ Future<void> importSecretsConfig() async {
final solanaOutput = final solanaOutput =
solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput)); solanaInput.keys.fold('', (String acc, String val) => acc + generateConst(val, solanaInput));
final tronOutputFile = File(tronOutputPath);
final tronInput = json.decode(File(tronConfigPath).readAsStringSync()) as Map<String, dynamic>;
final tronOutput =
tronInput.keys.fold('', (String acc, String val) => acc + generateConst(val, tronInput));
if (outputFile.existsSync()) { if (outputFile.existsSync()) {
await outputFile.delete(); await outputFile.delete();
} }
@ -46,4 +54,10 @@ Future<void> importSecretsConfig() async {
} }
await solanaOutputFile.writeAsString(solanaOutput); await solanaOutputFile.writeAsString(solanaOutput);
if (tronOutputFile.existsSync()) {
await tronOutputFile.delete();
}
await tronOutputFile.writeAsString(tronOutput);
} }

View file

@ -50,6 +50,10 @@ class SecretKey {
SecretKey('ankrApiKey', () => ''), SecretKey('ankrApiKey', () => ''),
]; ];
static final tronSecrets = [
SecretKey('tronGridApiKey', () => ''),
];
final String name; final String name;
final String Function() generate; final String Function() generate;
} }