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

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