Merge branch 'main' of https://github.com/cake-tech/cake_wallet into bip39-minor-enhancements

This commit is contained in:
Matthew Fosse 2024-05-13 09:42:20 -07:00
commit 8ffde50953
249 changed files with 8737 additions and 1652 deletions

View file

@ -42,7 +42,7 @@ jobs:
- name: Flutter action - name: Flutter action
uses: subosito/flutter-action@v1 uses: subosito/flutter-action@v1
with: with:
flutter-version: "3.10.x" flutter-version: "3.19.5"
channel: stable channel: stable
- name: Install package dependencies - name: Install package dependencies
@ -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,8 @@ 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 nano2ApiKey = '${{ secrets.NANO2_API_KEY }}';" >> cw_nano/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: |

5
.gitignore vendored
View file

@ -94,9 +94,12 @@ 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/.nano-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 +135,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
@ -152,6 +156,7 @@ assets/images/app_logo.png
macos/Runner/Info.plist macos/Runner/Info.plist
macos/Runner/DebugProfile.entitlements macos/Runner/DebugProfile.entitlements
macos/Runner/Release.entitlements macos/Runner/Release.entitlements
lib/core/secure_storage.dart
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png

View file

@ -9,6 +9,26 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--bibo01 : hardware option-->
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
<!-- required for API 18 - 30 -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<application <application
android:name=".Application" android:name=".Application"
@ -67,6 +87,16 @@
<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>
<!-- nano-gpt link scheme -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nano-gpt" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data <meta-data

View file

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.8.21'
repositories { repositories {
google() google()
jcenter() jcenter()

View file

@ -0,0 +1,5 @@
-
uri: kaliumapi.appditto.com
path: /api
useSSL: true
is_default: true

BIN
assets/images/bluetooth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/usb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,2 +1 @@
UI enhancements Generic bug fixes and enhancements
Bug fixes

View file

@ -1,7 +1,3 @@
Add Replace-By-Fee to boost pending Bitcoin transactions Hardware wallets support for Bitcoin, Ethereum and Polygon
Enable WalletConnect for Solana Security enhancements
WalletConnect Enhancements Bug fixes and generic enhancements
Enhancements for ERC-20 tokens and Solana tokens
Enhancements for Nano wallet
UI enhancements
Bug fixes

View file

@ -0,0 +1,8 @@
-
uri: tron-rpc.publicnode.com:443
is_default: true
useSSL: true
-
uri: api.trongrid.io
is_default: false
useSSL: true

View file

@ -0,0 +1,43 @@
import 'dart:async';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
class BitcoinHardwareWalletService {
BitcoinHardwareWalletService(this.ledger, this.device);
final Ledger ledger;
final LedgerDevice device;
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final bitcoinLedgerApp = BitcoinLedgerApp(ledger);
final masterFp = await bitcoinLedgerApp.getMasterFingerprint(device);
print(masterFp);
final accounts = <HardwareAccountData>[];
final indexRange = List.generate(limit, (i) => i + index);
for (final i in indexRange) {
final derivationPath = "m/84'/0'/$i'";
final xpub = await bitcoinLedgerApp.getXPubKey(device, derivationPath: derivationPath);
HDWallet hd = HDWallet.fromBase58(xpub).derive(0);
final address = generateP2WPKHAddress(hd: hd, index: 0, network: BitcoinNetwork.mainnet);
accounts.add(HardwareAccountData(
address: address,
accountIndex: i,
derivationPath: derivationPath,
masterFingerprint: masterFp,
xpub: xpub,
));
}
return accounts;
}
}

View file

@ -1,8 +1,13 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:convert/convert.dart';
import 'package:cw_bitcoin/bitcoin_mnemonic.dart'; import 'package:cw_bitcoin/bitcoin_mnemonic.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
@ -20,11 +25,12 @@ class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store { abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinWalletBase({ BitcoinWalletBase({
required String mnemonic,
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes, Uint8List? seedBytes,
String? mnemonic,
String? xpub,
String? addressPageType, String? addressPageType,
BasedUtxoNetwork? networkParam, BasedUtxoNetwork? networkParam,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
@ -33,32 +39,34 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
String? passphrase, String? passphrase,
}) : super( }) : super(
mnemonic: mnemonic, mnemonic: mnemonic,
passphrase: passphrase, passphrase: passphrase,
password: password, xpub: xpub,
walletInfo: walletInfo, password: password,
unspentCoinsInfo: unspentCoinsInfo, walletInfo: walletInfo,
networkType: networkParam == null unspentCoinsInfo: unspentCoinsInfo,
networkType: networkParam == null
? bitcoin.bitcoin
: networkParam == BitcoinNetwork.mainnet
? bitcoin.bitcoin ? bitcoin.bitcoin
: networkParam == BitcoinNetwork.mainnet : bitcoin.testnet,
? bitcoin.bitcoin initialAddresses: initialAddresses,
: bitcoin.testnet, initialBalance: initialBalance,
initialAddresses: initialAddresses, seedBytes: seedBytes,
initialBalance: initialBalance, currency: CryptoCurrency.btc) {
seedBytes: seedBytes, // in a standard BIP44 wallet, mainHd derivation path = m/84'/0'/0'/0 (account 0, index unspecified here)
currency: CryptoCurrency.btc, // the sideHd derivation path = m/84'/0'/0'/1 (account 1, index unspecified here)
) { // String derivationPath = walletInfo.derivationInfo!.derivationPath!;
String derivationPath = walletInfo.derivationInfo!.derivationPath! + "/0"; // String sideDerivationPath = derivationPath.substring(0, derivationPath.length - 1) + "1";
String sideDerivationPath = walletInfo.derivationInfo!.derivationPath! + "/1"; // final hd = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
final hd2 = bitcoin.HDWallet.fromSeed(seedBytes, network: networkType);
walletAddresses = BitcoinWalletAddresses( walletAddresses = BitcoinWalletAddresses(
walletInfo, walletInfo,
electrumClient: electrumClient, electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd2.derivePath(derivationPath), mainHd: hd,
sideHd: hd2.derivePath(sideDerivationPath), sideHd: accountHD.derive(1),
network: networkParam ?? network, network: networkParam ?? network,
); );
autorun((_) { autorun((_) {
@ -126,25 +134,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
); );
// set the default if not present: // set the default if not present:
walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'"; walletInfo.derivationInfo!.derivationPath = snp.derivationPath ?? "m/0'/0";
walletInfo.derivationInfo!.derivationType = snp.derivationType ?? DerivationType.electrum;
late Uint8List seedBytes; Uint8List? seedBytes = null;
switch (walletInfo.derivationInfo!.derivationType) { if (snp.mnemonic != null) {
case DerivationType.electrum: switch (walletInfo.derivationInfo!.derivationType) {
seedBytes = await mnemonicToSeedBytes(snp.mnemonic); case DerivationType.electrum:
break; seedBytes = await mnemonicToSeedBytes(snp.mnemonic!);
case DerivationType.bip39: break;
default: case DerivationType.bip39:
seedBytes = await bip39.mnemonicToSeed( default:
snp.mnemonic, seedBytes = await bip39.mnemonicToSeed(
passphrase: snp.passphrase ?? '', snp.mnemonic!,
); passphrase: snp.passphrase ?? '',
break; );
break;
}
} }
return BitcoinWallet( return BitcoinWallet(
mnemonic: snp.mnemonic, mnemonic: snp.mnemonic,
xpub: snp.xpub,
password: password, password: password,
passphrase: snp.passphrase, passphrase: snp.passphrase,
walletInfo: walletInfo, walletInfo: walletInfo,
@ -158,4 +170,49 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
networkParam: network, networkParam: network,
); );
} }
Ledger? _ledger;
LedgerDevice? _ledgerDevice;
BitcoinLedgerApp? _bitcoinLedgerApp;
void setLedger(Ledger setLedger, LedgerDevice setLedgerDevice) {
_ledger = setLedger;
_ledgerDevice = setLedgerDevice;
_bitcoinLedgerApp = BitcoinLedgerApp(_ledger!, derivationPath: walletInfo.derivationInfo!.derivationPath!);
}
@override
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async {
final masterFingerprint = await _bitcoinLedgerApp!.getMasterFingerprint(_ledgerDevice!);
final psbtReadyInputs = <PSBTReadyUtxoWithAddress>[];
for (final utxo in utxos) {
final rawTx = await electrumClient.getTransactionHex(hash: utxo.utxo.txHash);
final publicKeyAndDerivationPath = publicKeys[utxo.ownerDetails.address.pubKeyHash()]!;
psbtReadyInputs.add(PSBTReadyUtxoWithAddress(
utxo: utxo.utxo,
rawTx: rawTx,
ownerDetails: utxo.ownerDetails,
ownerDerivationPath: publicKeyAndDerivationPath.derivationPath,
ownerMasterFingerprint: masterFingerprint,
ownerPublicKey: publicKeyAndDerivationPath.publicKey,
));
}
final psbt = PSBTTransactionBuild(inputs: psbtReadyInputs, outputs: outputs, enableRBF: enableRBF);
final rawHex = await _bitcoinLedgerApp!.signPsbt(_ledgerDevice!, psbt: psbt.psbt);
return BtcTransaction.fromRaw(hex.encode(rawHex));
}
} }

View file

@ -1,3 +1,4 @@
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -36,9 +37,22 @@ class BitcoinRestoreWalletFromSeedCredentials extends WalletCredentials {
} }
class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials { class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
BitcoinRestoreWalletFromWIFCredentials( BitcoinRestoreWalletFromWIFCredentials({
{required String name, required String password, required this.wif, WalletInfo? walletInfo}) required String name,
: super(name: name, password: password, walletInfo: walletInfo); required String password,
required this.wif,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String wif; final String wif;
} }
class BitcoinRestoreWalletFromHardware extends WalletCredentials {
BitcoinRestoreWalletFromHardware({
required String name,
required this.hwAccountData,
WalletInfo? walletInfo,
}) : super(name: name, walletInfo: walletInfo);
final HardwareAccountData hwAccountData;
}

View file

@ -14,8 +14,11 @@ import 'package:hive/hive.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService<BitcoinNewWalletCredentials, class BitcoinWalletService extends WalletService<
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials,
BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -99,9 +102,28 @@ class BitcoinWalletService extends WalletService<BitcoinNewWalletCredentials,
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<BitcoinWallet> restoreFromHardwareWallet(BitcoinRestoreWalletFromHardware credentials,
{bool? isTestnet}) async {
final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
credentials.walletInfo?.derivationInfo?.derivationPath = credentials.hwAccountData.derivationPath;
final wallet = await BitcoinWallet(password: credentials.password!,
xpub: credentials.hwAccountData.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
);
await wallet.save();
await wallet.init();
return wallet;
}
@override @override
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials,
{bool? isTestnet}) async => {bool? isTestnet}) async =>
throw UnimplementedError(); throw UnimplementedError();
@override @override

View file

@ -4,8 +4,8 @@ import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base; import 'package:bitcoin_base/bitcoin_base.dart' as bitcoin_base;
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
@ -37,9 +37,9 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:http/http.dart' as http;
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
import 'package:http/http.dart' as http;
part 'electrum_wallet.g.dart'; part 'electrum_wallet.g.dart';
@ -53,17 +53,16 @@ abstract class ElectrumWalletBase
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required this.networkType, required this.networkType,
required this.mnemonic, String? xpub,
required Uint8List seedBytes, String? mnemonic,
Uint8List? seedBytes,
this.passphrase, this.passphrase,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumClient? electrumClient, ElectrumClient? electrumClient,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
CryptoCurrency? currency}) CryptoCurrency? currency})
: hd = currency == CryptoCurrency.bch : accountHD =
? bitcoinCashHDWallet(seedBytes) getAccountHDWallet(currency, networkType, seedBytes, xpub, walletInfo.derivationInfo),
: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
.derivePath(walletInfo.derivationInfo?.derivationPath ?? "m/0'"),
syncStatus = NotConnectedSyncStatus(), syncStatus = NotConnectedSyncStatus(),
_password = password, _password = password,
_feeRates = <int>[], _feeRates = <int>[],
@ -80,20 +79,44 @@ abstract class ElectrumWalletBase
this.unspentCoinsInfo = unspentCoinsInfo, this.unspentCoinsInfo = unspentCoinsInfo,
this.network = _getNetwork(networkType, currency), this.network = _getNetwork(networkType, currency),
this.isTestnet = networkType == bitcoin.testnet, this.isTestnet = networkType == bitcoin.testnet,
this._mnemonic = mnemonic,
super(walletInfo) { super(walletInfo) {
this.electrumClient = electrumClient ?? ElectrumClient(); this.electrumClient = electrumClient ?? ElectrumClient();
this.walletInfo = walletInfo; this.walletInfo = walletInfo;
transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
} }
static bitcoin.HDWallet getAccountHDWallet(
CryptoCurrency? currency,
bitcoin.NetworkType networkType,
Uint8List? seedBytes,
String? xpub,
DerivationInfo? derivationInfo) {
if (seedBytes == null && xpub == null) {
throw Exception(
"To create a Wallet you need either a seed or an xpub. This should not happen");
}
if (seedBytes != null) {
return currency == CryptoCurrency.bch
? bitcoinCashHDWallet(seedBytes)
: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType)
.derivePath(_hardenedDerivationPath(derivationInfo?.derivationPath ?? "m/0'"));
}
return bitcoin.HDWallet.fromBase58(xpub!);
}
static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) => static bitcoin.HDWallet bitcoinCashHDWallet(Uint8List seedBytes) =>
bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/0"); bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'");
static int estimatedTransactionSize(int inputsCount, int outputsCounts) => static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10; inputsCount * 68 + outputsCounts * 34 + 10;
final bitcoin.HDWallet hd; final bitcoin.HDWallet accountHD;
final String mnemonic; final String? _mnemonic;
bitcoin.HDWallet get hd => accountHD.derive(0);
final String? passphrase; final String? passphrase;
@override @override
@ -123,10 +146,10 @@ abstract class ElectrumWalletBase
.map((addr) => scriptHash(addr.address, network: network)) .map((addr) => scriptHash(addr.address, network: network))
.toList(); .toList();
String get xpub => hd.base58!; String get xpub => accountHD.base58!;
@override @override
String get seed => mnemonic; String? get seed => _mnemonic;
bitcoin.NetworkType networkType; bitcoin.NetworkType networkType;
BasedUtxoNetwork network; BasedUtxoNetwork network;
@ -203,7 +226,9 @@ abstract class ElectrumWalletBase
int credentialsAmount = 0, int credentialsAmount = 0,
}) async { }) async {
final utxos = <UtxoWithAddress>[]; final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = []; final privateKeys = <ECPrivate>[];
final publicKeys = <String, PublicKeyWithDerivationPath>{};
int allInputsAmount = 0; int allInputsAmount = 0;
bool spendsUnconfirmedTX = false; bool spendsUnconfirmedTX = false;
@ -217,12 +242,22 @@ abstract class ElectrumWalletBase
allInputsAmount += utx.value; allInputsAmount += utx.value;
final address = addressTypeFromStr(utx.address, network); final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate( final hd =
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
index: utx.bitcoinAddressRecord.index, final derivationPath =
network: network); "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
privateKeys.add(privkey); publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
if (!walletInfo.isHardwareWallet) {
final privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
privateKeys.add(privkey);
}
utxos.add( utxos.add(
UtxoWithAddress( UtxoWithAddress(
@ -233,7 +268,7 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(address), scriptType: _getScriptType(address),
), ),
ownerDetails: UtxoAddressDetails( ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(), publicKey: pubKeyHex,
address: address, address: address,
), ),
), ),
@ -294,6 +329,7 @@ abstract class ElectrumWalletBase
return EstimatedTxResult( return EstimatedTxResult(
utxos: utxos, utxos: utxos,
privateKeys: privateKeys, privateKeys: privateKeys,
publicKeys: publicKeys,
fee: fee, fee: fee,
amount: amount, amount: amount,
isSendAll: true, isSendAll: true,
@ -312,7 +348,9 @@ abstract class ElectrumWalletBase
bool? useUnconfirmed, bool? useUnconfirmed,
}) async { }) async {
final utxos = <UtxoWithAddress>[]; final utxos = <UtxoWithAddress>[];
List<ECPrivate> privateKeys = []; final privateKeys = <ECPrivate>[];
final publicKeys = <String, PublicKeyWithDerivationPath>{};
int allInputsAmount = 0; int allInputsAmount = 0;
bool spendsUnconfirmedTX = false; bool spendsUnconfirmedTX = false;
@ -332,12 +370,23 @@ abstract class ElectrumWalletBase
leftAmount = leftAmount - utx.value; leftAmount = leftAmount - utx.value;
final address = addressTypeFromStr(utx.address, network); final address = addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
privateKeys.add(privkey); final hd =
utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd;
final derivationPath =
"${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}"
"/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}"
"/${utx.bitcoinAddressRecord.index}";
final pubKeyHex = hd.derive(utx.bitcoinAddressRecord.index).pubKey!;
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
if (!walletInfo.isHardwareWallet) {
final privkey =
generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network);
privateKeys.add(privkey);
}
utxos.add( utxos.add(
UtxoWithAddress( UtxoWithAddress(
@ -348,7 +397,7 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(address), scriptType: _getScriptType(address),
), ),
ownerDetails: UtxoAddressDetails( ownerDetails: UtxoAddressDetails(
publicKey: privkey.getPublic().toHex(), publicKey: pubKeyHex,
address: address, address: address,
), ),
), ),
@ -490,6 +539,7 @@ abstract class ElectrumWalletBase
return EstimatedTxResult( return EstimatedTxResult(
utxos: utxos, utxos: utxos,
privateKeys: privateKeys, privateKeys: privateKeys,
publicKeys: publicKeys,
fee: fee, fee: fee,
amount: amount, amount: amount,
hasChange: true, hasChange: true,
@ -557,6 +607,35 @@ abstract class ElectrumWalletBase
); );
} }
if (walletInfo.isHardwareWallet) {
final transaction = await buildHardwareWalletTransaction(
utxos: estimatedTx.utxos,
outputs: outputs,
publicKeys: estimatedTx.publicKeys,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
return PendingBitcoinTransaction(
transaction,
type,
electrumClient: electrumClient,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
network: network,
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
});
}
BasedBitcoinTransacationBuilder txb; BasedBitcoinTransacationBuilder txb;
if (network is BitcoinCashNetwork) { if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder( txb = ForkedTransactionBuilder(
@ -618,8 +697,22 @@ abstract class ElectrumWalletBase
} }
} }
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async =>
throw UnimplementedError();
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': mnemonic, 'mnemonic': _mnemonic,
'xpub': xpub,
'passphrase': passphrase ?? '', 'passphrase': passphrase ?? '',
'account_index': walletAddresses.currentReceiveAddressIndexByType, 'account_index': walletAddresses.currentReceiveAddressIndexByType,
'change_address_index': walletAddresses.currentChangeAddressIndexByType, 'change_address_index': walletAddresses.currentChangeAddressIndexByType,
@ -1263,7 +1356,7 @@ abstract class ElectrumWalletBase
void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError; void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
@override @override
String signMessage(String message, {String? address = null}) { Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null final index = address != null
? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index ? walletAddresses.allAddresses.firstWhere((element) => element.address == address).index
: null; : null;
@ -1286,6 +1379,9 @@ abstract class ElectrumWalletBase
return BitcoinNetwork.mainnet; return BitcoinNetwork.mainnet;
} }
static String _hardenedDerivationPath(String derivationPath) =>
derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1);
} }
class EstimateTxParams { class EstimateTxParams {
@ -1307,6 +1403,7 @@ class EstimatedTxResult {
EstimatedTxResult({ EstimatedTxResult({
required this.utxos, required this.utxos,
required this.privateKeys, required this.privateKeys,
required this.publicKeys,
required this.fee, required this.fee,
required this.amount, required this.amount,
required this.hasChange, required this.hasChange,
@ -1317,6 +1414,7 @@ class EstimatedTxResult {
final List<UtxoWithAddress> utxos; final List<UtxoWithAddress> utxos;
final List<ECPrivate> privateKeys; final List<ECPrivate> privateKeys;
final Map<String, PublicKeyWithDerivationPath> publicKeys; // PubKey to derivationPath
final int fee; final int fee;
final int amount; final int amount;
final bool hasChange; final bool hasChange;
@ -1325,6 +1423,13 @@ class EstimatedTxResult {
final bool spendsUnconfirmedTX; final bool spendsUnconfirmedTX;
} }
class PublicKeyWithDerivationPath {
const PublicKeyWithDerivationPath(this.publicKey, this.derivationPath);
final String derivationPath;
final String publicKey;
}
BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) { BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (network is BitcoinCashNetwork) { if (network is BitcoinCashNetwork) {
if (!address.startsWith("bitcoincash:") && if (!address.startsWith("bitcoincash:") &&

View file

@ -13,6 +13,7 @@ class ElectrumWalletSnapshot {
required this.type, required this.type,
required this.password, required this.password,
required this.mnemonic, required this.mnemonic,
required this.xpub,
required this.addresses, required this.addresses,
required this.balance, required this.balance,
required this.regularAddressIndex, required this.regularAddressIndex,
@ -28,7 +29,8 @@ class ElectrumWalletSnapshot {
final WalletType type; final WalletType type;
final String? addressPageType; final String? addressPageType;
String mnemonic; String? mnemonic;
String? xpub;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
ElectrumBalance balance; ElectrumBalance balance;
Map<String, int> regularAddressIndex; Map<String, int> regularAddressIndex;
@ -43,7 +45,8 @@ class ElectrumWalletSnapshot {
final jsonSource = await read(path: path, password: password); final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ?? <Object>[]; final addressesTmp = data['addresses'] as List? ?? <Object>[];
final mnemonic = data['mnemonic'] as String; final mnemonic = data['mnemonic'] as String?;
final xpub = data['xpub'] as String?;
final passphrase = data['passphrase'] as String? ?? ''; final passphrase = data['passphrase'] as String? ?? '';
final addresses = addressesTmp final addresses = addressesTmp
.whereType<String>() .whereType<String>()
@ -79,6 +82,7 @@ class ElectrumWalletSnapshot {
password: password, password: password,
passphrase: passphrase, passphrase: passphrase,
mnemonic: mnemonic, mnemonic: mnemonic,
xpub: xpub,
addresses: addresses, addresses: addresses,
balance: balance, balance: balance,
regularAddressIndex: regularAddressIndexByType, regularAddressIndex: regularAddressIndexByType,

View file

@ -15,6 +15,7 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/litecoin_network.dart'; import 'package:cw_bitcoin/litecoin_network.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bip39/bip39.dart' as bip39;
part 'litecoin_wallet.g.dart'; part 'litecoin_wallet.g.dart';
@ -52,8 +53,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd2.derivePath(derivationPath), mainHd: hd,
sideHd: hd2.derivePath(sideDerivationPath), sideHd: accountHD.derive(1),
network: network, network: network,
); );
autorun((_) { autorun((_) {
@ -66,11 +67,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
String? passphrase,
String? addressPageType, String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex}) async { Map<String, int>? initialChangeAddressIndex}) async {
late Uint8List seedBytes;
switch (walletInfo.derivationInfo?.derivationType) {
case DerivationType.bip39:
seedBytes = await bip39.mnemonicToSeed(
mnemonic,
passphrase: passphrase ?? "",
);
break;
case DerivationType.electrum:
default:
seedBytes = await mnemonicToSeedBytes(mnemonic);
break;
}
return LitecoinWallet( return LitecoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
@ -78,7 +94,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: await mnemonicToSeedBytes(mnemonic), seedBytes: seedBytes,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType, addressPageType: addressPageType,
@ -94,13 +110,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final snp = final snp =
await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet); await ElectrumWalletSnapshot.load(name, walletInfo.type, password, LitecoinNetwork.mainnet);
return LitecoinWallet( return LitecoinWallet(
mnemonic: snp.mnemonic, mnemonic: snp.mnemonic!,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp.addresses,
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic), seedBytes: await mnemonicToSeedBytes(snp.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType, addressPageType: snp.addressPageType,

View file

@ -16,7 +16,7 @@ import 'package:bip39/bip39.dart' as bip39;
class LitecoinWalletService extends WalletService< class LitecoinWalletService extends WalletService<
BitcoinNewWalletCredentials, BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials> { BitcoinRestoreWalletFromWIFCredentials,BitcoinNewWalletCredentials> {
LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -30,6 +30,7 @@ class LitecoinWalletService extends WalletService<
final wallet = await LitecoinWalletBase.create( final wallet = await LitecoinWalletBase.create(
mnemonic: await generateElectrumMnemonic(), mnemonic: await generateElectrumMnemonic(),
password: credentials.password!, password: credentials.password!,
passphrase: credentials.passphrase,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource);
await wallet.save(); await wallet.save();
@ -93,6 +94,11 @@ class LitecoinWalletService extends WalletService<
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<LitecoinWallet> restoreFromHardwareWallet(BitcoinNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Litecoin wallet from a hardware wallet is not yet supported!");
}
@override @override
Future<LitecoinWallet> restoreFromKeys( Future<LitecoinWallet> restoreFromKeys(
BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async => BitcoinRestoreWalletFromWIFCredentials credentials, {bool? isTestnet}) async =>
@ -107,6 +113,7 @@ class LitecoinWalletService extends WalletService<
final wallet = await LitecoinWalletBase.create( final wallet = await LitecoinWalletBase.create(
password: credentials.password!, password: credentials.password!,
passphrase: credentials.passphrase,
mnemonic: credentials.mnemonic, mnemonic: credentials.mnemonic,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource); unspentCoinsInfo: unspentCoinsInfoSource);

View file

@ -0,0 +1,96 @@
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:convert/convert.dart';
import 'package:ledger_bitcoin/psbt.dart';
class PSBTTransactionBuild {
final PsbtV2 psbt = PsbtV2();
PSBTTransactionBuild(
{required List<PSBTReadyUtxoWithAddress> inputs, required List<BitcoinBaseOutput> outputs, bool enableRBF = true}) {
psbt.setGlobalTxVersion(2);
psbt.setGlobalInputCount(inputs.length);
psbt.setGlobalOutputCount(outputs.length);
for (var i = 0; i < inputs.length; i++) {
final input = inputs[i];
print(input.utxo.isP2tr());
print(input.utxo.isSegwit());
print(input.utxo.isP2shSegwit());
psbt.setInputPreviousTxId(i, Uint8List.fromList(hex.decode(input.utxo.txHash).reversed.toList()));
psbt.setInputOutputIndex(i, input.utxo.vout);
psbt.setInputSequence(i, enableRBF ? 0x1 : 0xffffffff);
if (input.utxo.isSegwit()) {
setInputSegwit(i, input);
} else if (input.utxo.isP2shSegwit()) {
setInputP2shSegwit(i, input);
} else if (input.utxo.isP2tr()) {
// ToDo: (Konsti) Handle Taproot Inputs
} else {
setInputP2pkh(i, input);
}
}
for (var i = 0; i < outputs.length; i++) {
final output = outputs[i];
if (output is BitcoinOutput) {
psbt.setOutputScript(i, Uint8List.fromList(output.address.toScriptPubKey().toBytes()));
psbt.setOutputAmount(i, output.value.toInt());
}
}
}
void setInputP2pkh(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray());
}
void setInputSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(
i,
Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint,
BIPPath.fromString(input.ownerDerivationPath).toPathArray());
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
}
void setInputP2shSegwit(int i, PSBTReadyUtxoWithAddress input) {
psbt.setInputNonWitnessUtxo(i, Uint8List.fromList(hex.decode(input.rawTx)));
psbt.setInputBip32Derivation(i, Uint8List.fromList(hex.decode(input.ownerPublicKey)),
input.ownerMasterFingerprint, BIPPath.fromString(input.ownerDerivationPath).toPathArray());
psbt.setInputRedeemScript(
i, Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
psbt.setInputWitnessUtxo(i, Uint8List.fromList(bigIntToUint64LE(input.utxo.value)),
Uint8List.fromList(input.ownerDetails.address.toScriptPubKey().toBytes()));
}
}
class PSBTReadyUtxoWithAddress extends UtxoWithAddress {
final String rawTx;
final String ownerDerivationPath;
final Uint8List ownerMasterFingerprint;
final String ownerPublicKey;
PSBTReadyUtxoWithAddress({
required super.utxo,
required this.rawTx,
required super.ownerDetails,
required this.ownerDerivationPath,
required this.ownerMasterFingerprint,
required this.ownerPublicKey,
});
}

View file

@ -260,6 +260,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.4" version: "2.2.4"
dart_varuint_bitcoin:
dependency: transitive
description:
name: dart_varuint_bitcoin
sha256: "4f0ccc9733fb54148b9d3688eea822b7aaabf5cc00025998f8c09a1d45b31b4b"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
encrypt: encrypt:
dependency: transitive dependency: transitive
description: description:
@ -313,6 +321,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1+1" version: "2.2.1+1"
flutter_reactive_ble:
dependency: transitive
description:
name: flutter_reactive_ble
sha256: "247e2efa76de203d1ba11335c13754b5b9d0504b5423e5b0c93a600f016b24e0"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -326,6 +342,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
functional_data:
dependency: transitive
description:
name: functional_data
sha256: aefdec4365452283b2a7cf420a3169654d51d3e9553069a22d76680d7a9d7c3d
url: "https://pub.dev"
source: hosted
version: "1.1.1"
glob: glob:
dependency: transitive dependency: transitive
description: description:
@ -422,6 +446,31 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.1" version: "4.8.1"
ledger_bitcoin:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: b6ed573cbeb57d5f0d39dfe4254bf9d15b620ab6
url: "https://github.com/cake-tech/ledger-bitcoin.git"
source: git
version: "0.0.1"
ledger_flutter:
dependency: "direct main"
description:
name: ledger_flutter
sha256: f1680060ed6ff78f275837e0024ccaf667715a59ba7aa29fa7354bc7752e71c8
url: "https://pub.dev"
source: hosted
version: "1.0.1"
ledger_usb:
dependency: transitive
description:
name: ledger_usb
sha256: "52c92d03a4cffe06c82921c8e2f79f3cdad6e1cf78e1e9ca35444196ff8f14c2"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@ -582,6 +631,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "01dd9bd0fa02548bf2ceee13545d4a0ec6046459d847b6b061d8a27237108a08"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -606,6 +663,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.3" version: "1.2.3"
reactive_ble_mobile:
dependency: transitive
description:
name: reactive_ble_mobile
sha256: "9ec2b4c9c725e439950838d551579750060258fbccd5536d0543b4d07d225798"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
reactive_ble_platform_interface:
dependency: transitive
description:
name: reactive_ble_platform_interface
sha256: "632c92401a2d69c9b94bd48f8fd47488a7013f3d1f9b291884350291a4a81813"
url: "https://pub.dev"
source: hosted
version: "5.3.1"
rxdart: rxdart:
dependency: "direct main" dependency: "direct main"
description: description:
@ -788,5 +861,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.0.0 <4.0.0" dart: ">=3.0.6 <4.0.0"
flutter: ">=3.10.0" flutter: ">=3.10.0"

View file

@ -35,15 +35,22 @@ dependencies:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v2 ref: cake-update-v2
blockchain_utils: ^2.1.1 blockchain_utils: ^2.1.1
ledger_flutter: ^1.0.1
ledger_bitcoin:
git:
url: https://github.com/cake-tech/ledger-bitcoin.git
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
build_resolvers: ^2.0.9 build_resolvers: ^2.0.9
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -51,7 +51,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
mainHd: hd, mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes).derivePath("m/44'/145'/0'/1"), sideHd: accountHD.derive(1),
network: network, network: network,
initialAddressPageType: addressPageType, initialAddressPageType: addressPageType,
); );
@ -93,7 +93,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
final snp = await ElectrumWalletSnapshot.load( final snp = await ElectrumWalletSnapshot.load(
name, walletInfo.type, password, BitcoinCashNetwork.mainnet); name, walletInfo.type, password, BitcoinCashNetwork.mainnet);
return BitcoinCashWallet( return BitcoinCashWallet(
mnemonic: snp.mnemonic, mnemonic: snp.mnemonic!,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
@ -118,7 +118,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
} }
}).toList(), }).toList(),
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: await Mnemonic.toSeed(snp.mnemonic), seedBytes: await Mnemonic.toSeed(snp.mnemonic!),
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex, initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: P2pkhAddressType.p2pkh, addressPageType: P2pkhAddressType.p2pkh,
@ -166,7 +166,7 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
} }
@override @override
String signMessage(String message, {String? address = null}) { Future<String> signMessage(String message, {String? address = null}) async {
final index = address != null final index = address != null
? walletAddresses.allAddresses ? walletAddresses.allAddresses
.firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address)) .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))

View file

@ -12,7 +12,7 @@ import 'package:collection/collection.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredentials, class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredentials,
BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials> { BitcoinCashRestoreWalletFromSeedCredentials, BitcoinCashRestoreWalletFromWIFCredentials, BitcoinCashNewWalletCredentials> {
BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); BitcoinCashWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -93,6 +93,11 @@ class BitcoinCashWalletService extends WalletService<BitcoinCashNewWalletCredent
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<BitcoinCashWallet> restoreFromHardwareWallet(BitcoinCashNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Bitcoin Cash wallet from a hardware wallet is not yet supported!");
}
@override @override
Future<BitcoinCashWallet> restoreFromKeys(credentials, {bool? isTestnet}) { Future<BitcoinCashWallet> restoreFromKeys(credentials, {bool? isTestnet}) {
// TODO: implement restoreFromKeys // TODO: implement restoreFromKeys

View file

@ -39,10 +39,13 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -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 =
@ -257,10 +259,16 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
element.tag == walletCurrency?.tag)); element.tag == walletCurrency?.tag));
} catch (_) {} } catch (_) {}
// search by fullName if not found by title:
try {
return CryptoCurrency.all.firstWhere((element) => element.fullName?.toLowerCase() == name);
} catch (_) {}
if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) { if (CryptoCurrency._nameCurrencyMap[name.toLowerCase()] == null) {
final s = 'Unexpected token: $name for CryptoCurrency fromString'; final s = 'Unexpected token: $name for CryptoCurrency fromString';
throw ArgumentError.value(name, 'name', s); throw ArgumentError.value(name, 'name', s);
} }
return CryptoCurrency._nameCurrencyMap[name.toLowerCase()]!; return CryptoCurrency._nameCurrencyMap[name.toLowerCase()]!;
} }

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

@ -0,0 +1,28 @@
import 'package:cw_core/wallet_type.dart';
enum DeviceConnectionType {
usb,
ble;
static List<DeviceConnectionType> supportedConnectionTypes(WalletType walletType,
[bool isIOS = false]) {
switch (walletType) {
case WalletType.bitcoin:
case WalletType.ethereum:
case WalletType.polygon:
if (isIOS) return [DeviceConnectionType.ble];
return [DeviceConnectionType.ble, DeviceConnectionType.usb];
default:
return [];
}
}
String get iconString {
switch (this) {
case ble:
return 'assets/images/bluetooth.png';
case usb:
return 'assets/images/usb.png';
}
}
}

View file

@ -0,0 +1,7 @@
class DeviceNotConnectedException implements Exception {
final String message;
DeviceNotConnectedException({
this.message = '',
});
}

View file

@ -0,0 +1,19 @@
import 'dart:typed_data';
class HardwareAccountData {
HardwareAccountData({
required this.address,
required this.accountIndex,
required this.derivationPath,
this.xpub,
this.masterFingerprint,
});
final String address;
final int accountIndex;
final String derivationPath;
// Bitcoin Specific
final Uint8List? masterFingerprint;
final String? xpub;
}

View file

@ -16,3 +16,5 @@ 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;
const HARDWARE_WALLET_TYPE_TYPE_ID = 19;

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

@ -56,6 +56,8 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
bool get isEnabledAutoGenerateSubaddress => false; bool get isEnabledAutoGenerateSubaddress => false;
bool get isHardwareWallet => walletInfo.isHardwareWallet;
Future<void> connectToNode({required Node node}); Future<void> connectToNode({required Node node});
// there is a default definition here because only coins with a pow node (nano based) need to override this // there is a default definition here because only coins with a pow node (nano based) need to override this
@ -88,7 +90,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<void> renameWalletFiles(String newWalletName); Future<void> renameWalletFiles(String newWalletName);
String signMessage(String message, {String? address = null}) => throw UnimplementedError(); Future<String> signMessage(String message, {String? address = null}) => throw UnimplementedError();
bool? isTestnet; bool? isTestnet;
} }

View file

@ -9,6 +9,7 @@ abstract class WalletCredentials {
this.password, this.password,
this.passphrase, this.passphrase,
this.derivationInfo, this.derivationInfo,
this.hardwareWalletType,
}) { }) {
if (this.walletInfo != null && derivationInfo != null) { if (this.walletInfo != null && derivationInfo != null) {
this.walletInfo!.derivationInfo = derivationInfo; this.walletInfo!.derivationInfo = derivationInfo;
@ -22,4 +23,5 @@ abstract class WalletCredentials {
String? passphrase; String? passphrase;
WalletInfo? walletInfo; WalletInfo? walletInfo;
DerivationInfo? derivationInfo; DerivationInfo? derivationInfo;
HardwareWalletType? hardwareWalletType;
} }

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:cw_core/address_info.dart'; import 'package:cw_core/address_info.dart';
import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -20,6 +21,12 @@ enum DerivationType {
electrum, electrum,
} }
@HiveType(typeId: HARDWARE_WALLET_TYPE_TYPE_ID)
enum HardwareWalletType {
@HiveField(0)
ledger,
}
@HiveType(typeId: DerivationInfo.typeId) @HiveType(typeId: DerivationInfo.typeId)
class DerivationInfo extends HiveObject { class DerivationInfo extends HiveObject {
DerivationInfo({ DerivationInfo({
@ -40,7 +47,7 @@ class DerivationInfo extends HiveObject {
@HiveField(1, defaultValue: '') @HiveField(1, defaultValue: '')
String balance; String balance;
@HiveField(2) @HiveField(2, defaultValue: 0)
int transactionsCount; int transactionsCount;
@HiveField(3) @HiveField(3)
@ -71,8 +78,9 @@ class WalletInfo extends HiveObject {
this.yatEid, this.yatEid,
this.yatLastUsedAddressRaw, this.yatLastUsedAddressRaw,
this.showIntroCakePayCard, this.showIntroCakePayCard,
this.derivationInfo) this.derivationInfo,
: _yatLastUsedAddressController = StreamController<String>.broadcast(); this.hardwareWalletType,
): _yatLastUsedAddressController = StreamController<String>.broadcast();
factory WalletInfo.external({ factory WalletInfo.external({
required String id, required String id,
@ -88,6 +96,7 @@ class WalletInfo extends HiveObject {
String yatEid = '', String yatEid = '',
String yatLastUsedAddressRaw = '', String yatLastUsedAddressRaw = '',
DerivationInfo? derivationInfo, DerivationInfo? derivationInfo,
HardwareWalletType? hardwareWalletType,
}) { }) {
return WalletInfo( return WalletInfo(
id, id,
@ -103,6 +112,7 @@ class WalletInfo extends HiveObject {
yatLastUsedAddressRaw, yatLastUsedAddressRaw,
showIntroCakePayCard, showIntroCakePayCard,
derivationInfo, derivationInfo,
hardwareWalletType,
); );
} }
@ -154,9 +164,11 @@ class WalletInfo extends HiveObject {
@HiveField(15) @HiveField(15)
List<String>? usedAddresses; List<String>? usedAddresses;
@deprecated
@HiveField(16) @HiveField(16)
DerivationType? derivationType; // no longer used DerivationType? derivationType; // no longer used
@deprecated
@HiveField(17) @HiveField(17)
String? derivationPath; // no longer used String? derivationPath; // no longer used
@ -169,6 +181,9 @@ class WalletInfo extends HiveObject {
@HiveField(20) @HiveField(20)
DerivationInfo? derivationInfo; DerivationInfo? derivationInfo;
@HiveField(21)
HardwareWalletType? hardwareWalletType;
String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; String get yatLastUsedAddress => yatLastUsedAddressRaw ?? '';
set yatLastUsedAddress(String address) { set yatLastUsedAddress(String address) {
@ -185,6 +200,8 @@ class WalletInfo extends HiveObject {
return showIntroCakePayCard!; return showIntroCakePayCard!;
} }
bool get isHardwareWallet => hardwareWalletType != null;
DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp); DateTime get date => DateTime.fromMillisecondsSinceEpoch(timestamp);
Stream<String> get yatLastUsedAddressStream => _yatLastUsedAddressController.stream; Stream<String> get yatLastUsedAddressStream => _yatLastUsedAddressController.stream;

View file

@ -6,11 +6,13 @@ import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials, abstract class WalletService<N extends WalletCredentials, RFS extends WalletCredentials,
RFK extends WalletCredentials> { RFK extends WalletCredentials, RFH extends WalletCredentials> {
WalletType getType(); WalletType getType();
Future<WalletBase> create(N credentials, {bool? isTestnet}); Future<WalletBase> create(N credentials, {bool? isTestnet});
Future<WalletBase> restoreFromHardwareWallet(RFH credentials);
Future<WalletBase> restoreFromSeed(RFS credentials, {bool? isTestnet}); Future<WalletBase> restoreFromSeed(RFS credentials, {bool? isTestnet});
Future<WalletBase> restoreFromKeys(RFK credentials, {bool? isTestnet}); Future<WalletBase> restoreFromKeys(RFK credentials, {bool? isTestnet});

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

View file

@ -0,0 +1,22 @@
import 'dart:io';
import 'package:flutter/services.dart';
const MethodChannel _channel = MethodChannel('com.cake_wallet/native_utils');
Future<void> setDefaultMinimumWindowSize() async {
if (!Platform.isMacOS) return;
try {
final result = await _channel.invokeMethod(
'setMinWindowSize',
{'width': 500, 'height': 700},
) as bool;
if (!result) {
print("Failed to set minimum window size.");
}
} on PlatformException catch (e) {
print("Failed to set minimum window size: '${e.message}'.");
}
}

View file

@ -28,11 +28,13 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
build_resolvers: ^2.0.9 build_resolvers: ^2.0.9
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^2.0.1 hive_generator: ^2.0.1
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -1,11 +1,12 @@
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_ethereum/ethereum_client.dart'; import 'package:cw_ethereum/ethereum_client.dart';
import 'package:cw_ethereum/ethereum_mnemonics_exception.dart'; import 'package:cw_ethereum/ethereum_mnemonics_exception.dart';
import 'package:cw_ethereum/ethereum_wallet.dart'; import 'package:cw_ethereum/ethereum_wallet.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_evm/evm_chain_wallet_service.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart';
import 'package:bip39/bip39.dart' as bip39;
class EthereumWalletService extends EVMChainWalletService<EthereumWallet> { class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
EthereumWalletService(super.walletInfoSource, {required this.client}); EthereumWalletService(super.walletInfoSource, {required this.client});
@ -82,6 +83,29 @@ class EthereumWalletService extends EVMChainWalletService<EthereumWallet> {
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<EthereumWallet> restoreFromHardwareWallet(
EVMChainRestoreWalletFromHardware credentials) async {
credentials.walletInfo!.derivationInfo = DerivationInfo(
derivationType: DerivationType.bip39,
derivationPath: "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"
);
credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType;
credentials.walletInfo!.address = credentials.hwAccountData.address;
final wallet = EthereumWallet(
walletInfo: credentials.walletInfo!,
password: credentials.password!,
client: client,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override @override
Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials, Future<EthereumWallet> restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials,
{bool? isTestnet}) async { {bool? isTestnet}) async {

View file

@ -19,10 +19,18 @@ dependencies:
path: ../cw_evm path: ../cw_evm
hive: ^2.2.3 hive: ^2.2.3
dependency_overrides:
web3dart:
git:
url: https://github.com/cake-tech/web3dart.git
ref: cake
watcher: ^1.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
flutter: flutter:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg

View file

@ -0,0 +1,209 @@
import 'dart:typed_data';
import 'package:web3dart/web3dart.dart' as web3;
final _contractAbi = web3.ContractAbi.fromJson(
'[{"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":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"},{"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":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]',
'Erc20');
/// Interface of the ERC20 standard as defined in the EIP.
class ERC20 extends web3.GeneratedContract {
/// Constructor.
ERC20({
required web3.EthereumAddress address,
required web3.Web3Client client,
int? chainId,
}) : super(web3.DeployedContract(_contractAbi, address), client, chainId);
/// Returns the remaining number of tokens that [spender] will be allowed to spend on behalf of [owner] through [transferFrom]. This is zero by default. This value changes when [approve] or [transferFrom] are called.
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<BigInt> allowance(
web3.EthereumAddress owner,
web3.EthereumAddress spender, {
web3.BlockNum? atBlock,
}) async {
final function = self.abi.functions[0];
assert(checkSignature(function, 'dd62ed3e'));
final params = [owner, spender];
final response = await read(function, params, atBlock);
return (response[0] as BigInt);
}
/// Sets [amount] as the allowance of [spender] over the caller's tokens. Returns a boolean value indicating whether the operation succeeded. IMPORTANT: Beware that changing an allowance with this method brings the risk that someone may use both the old and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 Emits an [Approval] event.
///
/// The optional [transaction] parameter can be used to override parameters
/// like the gas price, nonce and max gas. The `data` and `to` fields will be
/// set by the contract.
Future<Uint8List> approve(
web3.EthereumAddress spender,
BigInt amount, {
required web3.Credentials credentials,
web3.Transaction? transaction,
}) async {
final function = self.abi.functions[1];
assert(checkSignature(function, '095ea7b3'));
final params = [spender, amount];
return writeRaw(credentials, transaction, function, params);
}
/// Returns the amount of tokens owned by [account].
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<BigInt> balanceOf(
web3.EthereumAddress account, {
web3.BlockNum? atBlock,
}) async {
final function = self.abi.functions[2];
assert(checkSignature(function, '70a08231'));
final params = [account];
final response = await read(function, params, atBlock);
return (response[0] as BigInt);
}
/// Returns the decimal precision of the token.
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<BigInt> decimals({web3.BlockNum? atBlock}) async {
final function = self.abi.functions[3];
assert(checkSignature(function, '313ce567'));
final params = [];
final response = await read(function, params, atBlock);
return (response[0] as BigInt);
}
/// Returns the name of the token.
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<String> name({web3.BlockNum? atBlock}) async {
final function = self.abi.functions[4];
assert(checkSignature(function, '06fdde03'));
final params = [];
final response = await read(function, params, atBlock);
return (response[0] as String);
}
/// Returns the symbol of the token.
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<String> symbol({web3.BlockNum? atBlock}) async {
final function = self.abi.functions[5];
assert(checkSignature(function, '95d89b41'));
final params = [];
final response = await read(function, params, atBlock);
return (response[0] as String);
}
/// Returns the amount of tokens in existence.
///
/// The optional [atBlock] parameter can be used to view historical data. When
/// set, the function will be evaluated in the specified block. By default, the
/// latest on-chain block will be used.
Future<BigInt> totalSupply({web3.BlockNum? atBlock}) async {
final function = self.abi.functions[6];
assert(checkSignature(function, '18160ddd'));
final params = [];
final response = await read(function, params, atBlock);
return (response[0] as BigInt);
}
/// Moves [amount] tokens from the caller's account to [recipient]. Returns a boolean value indicating whether the operation succeeded. Emits a [Transfer] event.
///
/// The optional [transaction] parameter can be used to override parameters
/// like the gas price, nonce and max gas. The `data` and `to` fields will be
/// set by the contract.
Future<Uint8List> transfer(
web3.EthereumAddress recipient,
BigInt amount, {
required web3.Credentials credentials,
web3.Transaction? transaction,
}) async {
final function = self.abi.functions[7];
assert(checkSignature(function, 'a9059cbb'));
final params = [recipient, amount];
return writeRaw(credentials, transaction, function, params);
}
/// Moves [amount] tokens from [sender] to [recipient] using the allowance mechanism. [amount] is then deducted from the caller's allowance. Returns a boolean value indicating whether the operation succeeded. Emits a [Transfer] event.
///
/// The optional [transaction] parameter can be used to override parameters
/// like the gas price, nonce and max gas. The `data` and `to` fields will be
/// set by the contract.
Future<Uint8List> transferFrom(web3.EthereumAddress sender,
web3.EthereumAddress recipient, BigInt amount,
{required web3.Credentials credentials,
web3.Transaction? transaction}) async {
final function = self.abi.functions[8];
assert(checkSignature(function, '23b872dd'));
final params = [sender, recipient, amount];
return writeRaw(credentials, transaction, function, params);
}
/// Returns a live stream of all Approval events emitted by this contract.
Stream<Approval> approvalEvents(
{web3.BlockNum? fromBlock, web3.BlockNum? toBlock}) {
final event = self.event('Approval');
final filter = web3.FilterOptions.events(
contract: self, event: event, fromBlock: fromBlock, toBlock: toBlock);
return client.events(filter).map((web3.FilterEvent result) {
final decoded = event.decodeResults(result.topics!, result.data!);
return Approval._(decoded);
});
}
/// Returns a live stream of all Transfer events emitted by this contract.
Stream<Transfer> transferEvents(
{web3.BlockNum? fromBlock, web3.BlockNum? toBlock}) {
final event = self.event('Transfer');
final filter = web3.FilterOptions.events(
contract: self, event: event, fromBlock: fromBlock, toBlock: toBlock);
return client.events(filter).map((web3.FilterEvent result) {
final decoded = event.decodeResults(result.topics!, result.data!);
return Transfer._(decoded);
});
}
}
/// Emitted when the allowance of a [spender] for an [owner] is set by a call to [ERC20.approve]. [value] is the new allowance.
class Approval {
Approval._(List<dynamic> response)
: owner = (response[0] as web3.EthereumAddress),
spender = (response[1] as web3.EthereumAddress),
value = (response[2] as BigInt);
/// The owner address.
final web3.EthereumAddress owner;
/// The spender address.
final web3.EthereumAddress spender;
/// Value.
final BigInt value;
}
/// Emitted when [value] tokens are moved from one account ([from]) to another ([to]). Note that [value] may be zero.
class Transfer {
Transfer._(List<dynamic> response)
: from = (response[0] as web3.EthereumAddress),
to = (response[1] as web3.EthereumAddress),
value = (response[2] as BigInt);
/// From address.
final web3.EthereumAddress from;
/// To address.
final web3.EthereumAddress to;
/// Value.
final BigInt value;
}

View file

@ -2,21 +2,20 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'package:cw_core/node.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_evm/evm_erc20_balance.dart'; import 'package:cw_core/node.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_erc20_balance.dart';
import 'package:cw_evm/pending_evm_chain_transaction.dart';
import 'package:cw_evm/.secrets.g.dart' as secrets; import 'package:cw_evm/.secrets.g.dart' as secrets;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:http/http.dart';
import 'package:erc20/erc20.dart';
import 'package:web3dart/web3dart.dart';
import 'package:hex/hex.dart' as hex; import 'package:hex/hex.dart' as hex;
import 'package:http/http.dart';
import 'package:web3dart/web3dart.dart';
import 'contract/erc20.dart';
abstract class EVMChainClient { abstract class EVMChainClient {
final httpClient = Client(); final httpClient = Client();
@ -82,7 +81,7 @@ abstract class EVMChainClient {
} }
Future<PendingEVMChainTransaction> signTransaction({ Future<PendingEVMChainTransaction> signTransaction({
required EthPrivateKey privateKey, required Credentials privateKey,
required String toAddress, required String toAddress,
required BigInt amount, required BigInt amount,
required int gas, required int gas,
@ -96,8 +95,7 @@ abstract class EVMChainClient {
currency == CryptoCurrency.maticpoly || currency == CryptoCurrency.maticpoly ||
contractAddress != null); contractAddress != null);
bool isEVMCompatibleChain = bool isNativeToken = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly;
final price = _client!.getGasPrice(); final price = _client!.getGasPrice();
@ -105,17 +103,16 @@ abstract class EVMChainClient {
from: privateKey.address, from: privateKey.address,
to: EthereumAddress.fromHex(toAddress), to: EthereumAddress.fromHex(toAddress),
maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip),
amount: isEVMCompatibleChain ? EtherAmount.inWei(amount) : EtherAmount.zero(), amount: isNativeToken ? EtherAmount.inWei(amount) : EtherAmount.zero(),
data: data != null ? hexToBytes(data) : null, data: data != null ? hexToBytes(data) : null,
); );
final signedTransaction = Uint8List signedTransaction;
await _client!.signTransaction(privateKey, transaction, chainId: chainId);
final Function _sendTransaction; final Function _sendTransaction;
if (isEVMCompatibleChain) { if (isNativeToken) {
_sendTransaction = () async => await sendTransaction(signedTransaction); signedTransaction = await _client!.signTransaction(privateKey, transaction, chainId: chainId);
} else { } else {
final erc20 = ERC20( final erc20 = ERC20(
client: _client!, client: _client!,
@ -123,16 +120,17 @@ abstract class EVMChainClient {
chainId: chainId, chainId: chainId,
); );
_sendTransaction = () async { signedTransaction = await erc20.transfer(
await erc20.transfer( EthereumAddress.fromHex(toAddress),
EthereumAddress.fromHex(toAddress), amount,
amount, credentials: privateKey,
credentials: privateKey, transaction: transaction,
transaction: transaction, );
);
};
} }
_sendTransaction = () async => await sendTransaction(signedTransaction);
return PendingEVMChainTransaction( return PendingEVMChainTransaction(
signedTransaction: signedTransaction, signedTransaction: signedTransaction,
amount: amount.toString(), amount: amount.toString(),
@ -158,8 +156,9 @@ abstract class EVMChainClient {
); );
} }
Future<String> sendTransaction(Uint8List signedTransaction) async => Future<String> sendTransaction(Uint8List signedTransaction) async {
await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); return await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction));
}
Future getTransactionDetails(String transactionHash) async { Future getTransactionDetails(String transactionHash) async {
// Wait for the transaction receipt to become available // Wait for the transaction receipt to become available

View file

@ -6,6 +6,8 @@ class EVMChainTransactionCreationException implements Exception {
EVMChainTransactionCreationException(CryptoCurrency currency) EVMChainTransactionCreationException(CryptoCurrency currency)
: exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.';
EVMChainTransactionCreationException.fromMessage(this.exceptionMessage);
@override @override
String toString() => exceptionMessage; String toString() => exceptionMessage;
} }

View file

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:ledger_ethereum/ledger_ethereum.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
class EVMChainHardwareWalletService {
EVMChainHardwareWalletService(this.ledger, this.device);
final Ledger ledger;
final LedgerDevice device;
Future<List<HardwareAccountData>> getAvailableAccounts({int index = 0, int limit = 5}) async {
final ethereumLedgerApp = EthereumLedgerApp(ledger);
final version = await ethereumLedgerApp.getVersion(device);
final accounts = <HardwareAccountData>[];
final indexRange = List.generate(limit, (i) => i + index);
for (final i in indexRange) {
final derivationPath = "m/44'/60'/$i'/0/0";
final address =
await ethereumLedgerApp.getAccounts(device, accountsDerivationPath: derivationPath);
accounts.add(HardwareAccountData(
address: address.first,
accountIndex: i,
derivationPath: derivationPath,
));
}
return accounts;
}
}

View file

@ -1,6 +1,7 @@
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart'; import 'package:cw_core/output_info.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
class EVMChainTransactionCredentials { class EVMChainTransactionCredentials {
EVMChainTransactionCredentials( EVMChainTransactionCredentials(

View file

@ -25,6 +25,7 @@ import 'package:cw_evm/evm_chain_transaction_history.dart';
import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart'; import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_wallet_addresses.dart'; import 'package:cw_evm/evm_chain_wallet_addresses.dart';
import 'package:cw_evm/evm_ledger_credentials.dart';
import 'package:cw_evm/file.dart'; import 'package:cw_evm/file.dart';
import 'package:hex/hex.dart'; import 'package:hex/hex.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -83,9 +84,9 @@ abstract class EVMChainWalletBase
late final Box<Erc20Token> evmChainErc20TokensBox; late final Box<Erc20Token> evmChainErc20TokensBox;
late final EthPrivateKey _evmChainPrivateKey; late final Credentials _evmChainPrivateKey;
EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey; Credentials get evmChainPrivateKey => _evmChainPrivateKey;
late EVMChainClient _client; late EVMChainClient _client;
@ -141,12 +142,18 @@ abstract class EVMChainWalletBase
await walletAddresses.init(); await walletAddresses.init();
await transactionHistory.init(); await transactionHistory.init();
_evmChainPrivateKey = await getPrivateKey(
mnemonic: _mnemonic, if (walletInfo.isHardwareWallet) {
privateKey: _hexPrivateKey, _evmChainPrivateKey = EvmLedgerCredentials(walletInfo.address);
password: _password, walletAddresses.address = walletInfo.address;
); } else {
walletAddresses.address = _evmChainPrivateKey.address.hexEip55; _evmChainPrivateKey = await getPrivateKey(
mnemonic: _mnemonic,
privateKey: _hexPrivateKey,
password: _password,
);
walletAddresses.address = _evmChainPrivateKey.address.hexEip55;
}
await save(); await save();
} }
@ -283,6 +290,11 @@ abstract class EVMChainWalletBase
} }
} }
if (transactionCurrency is Erc20Token && isHardwareWallet) {
await (_evmChainPrivateKey as EvmLedgerCredentials)
.provideERC20Info(transactionCurrency.contractAddress, _client.chainId);
}
final pendingEVMChainTransaction = await _client.signTransaction( final pendingEVMChainTransaction = await _client.signTransaction(
privateKey: _evmChainPrivateKey, privateKey: _evmChainPrivateKey,
toAddress: _credentials.outputs.first.isParsedAddress toAddress: _credentials.outputs.first.isParsedAddress
@ -377,7 +389,9 @@ abstract class EVMChainWalletBase
String? get seed => _mnemonic; String? get seed => _mnemonic;
@override @override
String get privateKey => HEX.encode(_evmChainPrivateKey.privateKey); String? get privateKey => evmChainPrivateKey is EthPrivateKey
? HEX.encode((evmChainPrivateKey as EthPrivateKey).privateKey)
: null;
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
@ -535,8 +549,8 @@ abstract class EVMChainWalletBase
} }
@override @override
String signMessage(String message, {String? address}) => Future<String> signMessage(String message, {String? address}) async =>
bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); bytesToHex(await _evmChainPrivateKey.signPersonalMessage(ascii.encode(message)));
Web3Client? getWeb3Client() => _client.getWeb3Client(); Web3Client? getWeb3Client() => _client.getWeb3Client();
} }

View file

@ -1,3 +1,4 @@
import 'package:cw_core/hardware/hardware_account_data.dart';
import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -27,3 +28,13 @@ class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials {
final String privateKey; final String privateKey;
} }
class EVMChainRestoreWalletFromHardware extends WalletCredentials {
EVMChainRestoreWalletFromHardware({
required String name,
required this.hwAccountData,
WalletInfo? walletInfo,
}) : super(name: name, walletInfo: walletInfo);
final HardwareAccountData hwAccountData;
}

View file

@ -13,7 +13,8 @@ import 'package:hive/hive.dart';
abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletService< abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletService<
EVMChainNewWalletCredentials, EVMChainNewWalletCredentials,
EVMChainRestoreWalletFromSeedCredentials, EVMChainRestoreWalletFromSeedCredentials,
EVMChainRestoreWalletFromPrivateKey> { EVMChainRestoreWalletFromPrivateKey,
EVMChainRestoreWalletFromHardware> {
EVMChainWalletService(this.walletInfoSource); EVMChainWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -24,6 +25,9 @@ abstract class EVMChainWalletService<T extends EVMChainWallet> extends WalletSer
@override @override
Future<T> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet}); Future<T> create(EVMChainNewWalletCredentials credentials, {bool? isTestnet});
@override
Future<T> restoreFromHardwareWallet(EVMChainRestoreWalletFromHardware credentials);
@override @override
Future<T> openWallet(String name, String password); Future<T> openWallet(String name, String password);

View file

@ -0,0 +1,103 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:cw_core/hardware/device_not_connected_exception.dart';
import 'package:ledger_ethereum/ledger_ethereum.dart';
import 'package:ledger_flutter/ledger_flutter.dart';
import 'package:web3dart/crypto.dart';
import 'package:web3dart/web3dart.dart';
class EvmLedgerCredentials extends CredentialsWithKnownAddress {
final String _address;
Ledger? ledger;
LedgerDevice? ledgerDevice;
EthereumLedgerApp? ethereumLedgerApp;
EvmLedgerCredentials(this._address);
@override
EthereumAddress get address => EthereumAddress.fromHex(_address);
void setLedger(Ledger setLedger, [LedgerDevice? setLedgerDevice, String? derivationPath]) {
ledger = setLedger;
ledgerDevice = setLedgerDevice;
ethereumLedgerApp =
EthereumLedgerApp(ledger!, derivationPath: derivationPath ?? "m/44'/60'/0'/0/0");
}
@override
MsgSignature signToEcSignature(Uint8List payload, {int? chainId, bool isEIP1559 = false}) =>
throw UnimplementedError("EvmLedgerCredentials.signToEcSignature");
@override
Future<MsgSignature> signToSignature(Uint8List payload,
{int? chainId, bool isEIP1559 = false}) async {
if (ledgerDevice == null && ledger?.devices.isNotEmpty != true) {
throw DeviceNotConnectedException();
}
final sig = await ethereumLedgerApp!.signTransaction(device, payload);
final v = sig[0].toInt();
final r = bytesToHex(sig.sublist(1, 1 + 32));
final s = bytesToHex(sig.sublist(1 + 32, 1 + 32 + 32));
var truncChainId = chainId ?? 1;
while (truncChainId.bitLength > 32) {
truncChainId >>= 8;
}
final truncTarget = truncChainId * 2 + 35;
int parity = v;
if (truncTarget & 0xff == v) {
parity = 0;
} else if ((truncTarget + 1) & 0xff == v) {
parity = 1;
}
// https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L26
int chainIdV;
if (isEIP1559) {
chainIdV = v;
} else {
chainIdV = chainId != null ? (parity + (chainId * 2 + 35)) : parity;
}
return MsgSignature(BigInt.parse(r, radix: 16), BigInt.parse(s, radix: 16), chainIdV);
}
@override
Future<Uint8List> signPersonalMessage(Uint8List payload, {int? chainId}) async {
if (isNotConnected) throw DeviceNotConnectedException();
final sig = await ethereumLedgerApp!.signMessage(device, payload);
final r = sig.sublist(1, 1 + 32);
final s = sig.sublist(1 + 32, 1 + 32 + 32);
final v = [sig[0]];
// https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L63
return Uint8List.fromList(r + s + v);
}
@override
Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) =>
throw UnimplementedError("EvmLedgerCredentials.signPersonalMessageToUint8List");
Future<void> provideERC20Info(String erc20ContractAddress, int chainId) async {
if (isNotConnected) throw DeviceNotConnectedException();
try {
await ethereumLedgerApp!.getAndProvideERC20TokenInformation(device,
erc20ContractAddress: erc20ContractAddress, chainId: chainId);
} on LedgerException catch (e) {
if (e.errorCode != -28672) rethrow;
}
}
bool get isNotConnected => (ledgerDevice ?? ledger?.devices.firstOrNull) == null;
LedgerDevice get device => ledgerDevice ?? ledger!.devices.first;
}

View file

@ -13,7 +13,6 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
web3dart: ^2.7.1 web3dart: ^2.7.1
erc20: ^1.0.1
bip39: ^1.0.6 bip39: ^1.0.6
bip32: ^2.0.0 bip32: ^2.0.0
hex: ^0.2.0 hex: ^0.2.0
@ -23,11 +22,26 @@ dependencies:
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
cw_core: cw_core:
path: ../cw_core path: ../cw_core
ledger_flutter: ^1.0.1
ledger_ethereum:
git:
url: https://github.com/cake-tech/ledger-ethereum.git
dependency_overrides:
web3dart:
git:
url: https://github.com/cake-tech/web3dart.git
ref: cake
ledger_flutter:
git:
url: https://github.com/cake-tech/ledger-flutter.git
ref: cake
watcher: ^1.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0

View file

@ -56,7 +56,8 @@ class HavenRestoreWalletFromKeysCredentials extends WalletCredentials {
class HavenWalletService extends WalletService< class HavenWalletService extends WalletService<
HavenNewWalletCredentials, HavenNewWalletCredentials,
HavenRestoreWalletFromSeedCredentials, HavenRestoreWalletFromSeedCredentials,
HavenRestoreWalletFromKeysCredentials> { HavenRestoreWalletFromKeysCredentials,
HavenNewWalletCredentials> {
HavenWalletService(this.walletInfoSource); HavenWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -172,6 +173,11 @@ class HavenWalletService extends WalletService<
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<HavenWallet> restoreFromHardwareWallet(HavenNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Haven wallet from a hardware wallet is not yet supported!");
}
@override @override
Future<HavenWallet> restoreFromKeys( Future<HavenWallet> restoreFromKeys(
HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async { HavenRestoreWalletFromKeysCredentials credentials, {bool? isTestnet}) async {

View file

@ -24,11 +24,14 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
build_resolvers: ^2.0.9 build_resolvers: ^2.0.9
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -17,6 +17,9 @@ typedef restore_wallet_from_keys = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, P
typedef restore_wallet_from_spend_key = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, typedef restore_wallet_from_spend_key = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, Int32, Int64, Pointer<Utf8>); Pointer<Utf8>, Pointer<Utf8>, Int32, Int64, Pointer<Utf8>);
// typedef restore_wallet_from_device = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>,
// Int32, Int64, Pointer<Utf8>);
typedef is_wallet_exist = Int8 Function(Pointer<Utf8>); typedef is_wallet_exist = Int8 Function(Pointer<Utf8>);
typedef load_wallet = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Int8); typedef load_wallet = Int8 Function(Pointer<Utf8>, Pointer<Utf8>, Int8);

View file

@ -17,6 +17,9 @@ typedef RestoreWalletFromKeys = int Function(Pointer<Utf8>, Pointer<Utf8>,
typedef RestoreWalletFromSpendKey = int Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>, typedef RestoreWalletFromSpendKey = int Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>,
Pointer<Utf8>, Pointer<Utf8>, int, int, Pointer<Utf8>); Pointer<Utf8>, Pointer<Utf8>, int, int, Pointer<Utf8>);
typedef RestoreWalletFromDevice = int Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Utf8>,
int, int, Pointer<Utf8>);
typedef IsWalletExist = int Function(Pointer<Utf8>); typedef IsWalletExist = int Function(Pointer<Utf8>);
typedef LoadWallet = int Function(Pointer<Utf8>, Pointer<Utf8>, int); typedef LoadWallet = int Function(Pointer<Utf8>, Pointer<Utf8>, int);

View file

@ -31,6 +31,11 @@ final restoreWalletFromSpendKeyNative = moneroApi
'restore_wallet_from_spend_key') 'restore_wallet_from_spend_key')
.asFunction<RestoreWalletFromSpendKey>(); .asFunction<RestoreWalletFromSpendKey>();
// final restoreWalletFromDeviceNative = moneroApi
// .lookup<NativeFunction<restore_wallet_from_device>>(
// 'restore_wallet_from_device')
// .asFunction<RestoreWalletFromDevice>();
final isWalletExistNative = moneroApi final isWalletExistNative = moneroApi
.lookup<NativeFunction<is_wallet_exist>>('is_wallet_exist') .lookup<NativeFunction<is_wallet_exist>>('is_wallet_exist')
.asFunction<IsWalletExist>(); .asFunction<IsWalletExist>();
@ -185,6 +190,38 @@ void restoreWalletFromSpendKeySync(
} }
} }
// void restoreMoneroWalletFromDevice(
// {required String path,
// required String password,
// required String deviceName,
// int nettype = 0,
// int restoreHeight = 0}) {
//
// final pathPointer = path.toNativeUtf8();
// final passwordPointer = password.toNativeUtf8();
// final deviceNamePointer = deviceName.toNativeUtf8();
// final errorMessagePointer = ''.toNativeUtf8();
//
// final isWalletRestored = restoreWalletFromDeviceNative(
// pathPointer,
// passwordPointer,
// deviceNamePointer,
// nettype,
// restoreHeight,
// errorMessagePointer) != 0;
//
// calloc.free(pathPointer);
// calloc.free(passwordPointer);
//
// storeSync();
//
// if (!isWalletRestored) {
// throw WalletRestoreFromKeysException(
// message: convertUTF8ToString(pointer: errorMessagePointer));
// }
// }
void loadWallet({ void loadWallet({
required String path, required String path,
required String password, required String password,

View file

@ -686,7 +686,7 @@ abstract class MoneroWalletBase
void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e; void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e;
@override @override
String signMessage(String message, {String? address}) { Future<String> signMessage(String message, {String? address}) async {
final useAddress = address ?? ""; final useAddress = address ?? "";
return monero_wallet.signMessage(message, address: useAddress); return monero_wallet.signMessage(message, address: useAddress);
} }

View file

@ -55,7 +55,7 @@ class MoneroRestoreWalletFromKeysCredentials extends WalletCredentials {
} }
class MoneroWalletService extends WalletService<MoneroNewWalletCredentials, class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials> { MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials, MoneroNewWalletCredentials> {
MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -227,6 +227,11 @@ class MoneroWalletService extends WalletService<MoneroNewWalletCredentials,
} }
} }
@override
Future<MoneroWallet> restoreFromHardwareWallet(MoneroNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Monero wallet from a hardware wallet is not yet supported!");
}
@override @override
Future<MoneroWallet> restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials, Future<MoneroWallet> restoreFromSeed(MoneroRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async { {bool? isTestnet}) async {

View file

@ -26,11 +26,14 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
build_resolvers: ^2.0.9 build_resolvers: ^2.0.9
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -1,12 +1,28 @@
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
import 'package:nanoutil/nanoutil.dart'; import 'package:nanoutil/nanoutil.dart';
BigInt stringAmountToBigIntBanano(String amount) {
return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerBanano));
}
class BananoBalance extends Balance { class BananoBalance extends Balance {
final BigInt currentBalance; final BigInt currentBalance;
final BigInt receivableBalance; final BigInt receivableBalance;
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
BananoBalance.fromFormattedString(
{required String formattedCurrentBalance, required String formattedReceivableBalance})
: currentBalance = stringAmountToBigIntBanano(formattedCurrentBalance),
receivableBalance = stringAmountToBigIntBanano(formattedReceivableBalance),
super(0, 0);
BananoBalance.fromRawString(
{required String currentBalance, required String receivableBalance})
: currentBalance = BigInt.parse(currentBalance),
receivableBalance = BigInt.parse(receivableBalance),
super(0, 0);
@override @override
String get formattedAvailableBalance { String get formattedAvailableBalance {
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano); return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano);

View file

@ -1,7 +1,7 @@
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
import 'package:nanoutil/nanoutil.dart'; import 'package:nanoutil/nanoutil.dart';
BigInt stringAmountToBigInt(String amount) { BigInt stringAmountToBigIntNano(String amount) {
return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano)); return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano));
} }
@ -13,8 +13,8 @@ class NanoBalance extends Balance {
NanoBalance.fromFormattedString( NanoBalance.fromFormattedString(
{required String formattedCurrentBalance, required String formattedReceivableBalance}) {required String formattedCurrentBalance, required String formattedReceivableBalance})
: currentBalance = stringAmountToBigInt(formattedCurrentBalance), : currentBalance = stringAmountToBigIntNano(formattedCurrentBalance),
receivableBalance = stringAmountToBigInt(formattedReceivableBalance), receivableBalance = stringAmountToBigIntNano(formattedReceivableBalance),
super(0, 0); super(0, 0);
NanoBalance.fromRawString( NanoBalance.fromRawString(

View file

@ -10,6 +10,7 @@ import 'package:nanodart/nanodart.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'package:nanoutil/nanoutil.dart'; import 'package:nanoutil/nanoutil.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cw_nano/.secrets.g.dart' as secrets;
class NanoClient { class NanoClient {
static const Map<String, String> CAKE_HEADERS = { static const Map<String, String> CAKE_HEADERS = {
@ -52,10 +53,19 @@ class NanoClient {
} }
} }
Map<String, String> getHeaders() {
if (_node!.uri == "https://rpc.nano.to") {
return CAKE_HEADERS..addAll({
"key": secrets.nano2ApiKey,
});
}
return CAKE_HEADERS;
}
Future<NanoBalance> getBalance(String address) async { Future<NanoBalance> getBalance(String address) async {
final response = await http.post( final response = await http.post(
_node!.uri, _node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: jsonEncode( body: jsonEncode(
{ {
"action": "account_balance", "action": "account_balance",
@ -82,7 +92,7 @@ class NanoClient {
try { try {
final response = await http.post( final response = await http.post(
_node!.uri, _node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: jsonEncode( body: jsonEncode(
{ {
"action": "account_info", "action": "account_info",
@ -94,7 +104,7 @@ class NanoClient {
final data = await jsonDecode(response.body); final data = await jsonDecode(response.body);
return AccountInfoResponse.fromJson(data as Map<String, dynamic>); return AccountInfoResponse.fromJson(data as Map<String, dynamic>);
} catch (e) { } catch (e) {
print("error while getting account info"); print("error while getting account info $e");
return null; return null;
} }
} }
@ -149,7 +159,7 @@ class NanoClient {
Future<String> requestWork(String hash) async { Future<String> requestWork(String hash) async {
final response = await http.post( final response = await http.post(
_powNode!.uri, _powNode!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: json.encode( body: json.encode(
{ {
"action": "work_generate", "action": "work_generate",
@ -192,7 +202,7 @@ class NanoClient {
final processResponse = await http.post( final processResponse = await http.post(
_node!.uri, _node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: processBody, body: processBody,
); );
@ -351,7 +361,7 @@ class NanoClient {
}); });
final processResponse = await http.post( final processResponse = await http.post(
_node!.uri, _node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: processBody, body: processBody,
); );
@ -367,7 +377,7 @@ class NanoClient {
required String privateKey, required String privateKey,
}) async { }) async {
final receivableResponse = await http.post(_node!.uri, final receivableResponse = await http.post(_node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: jsonEncode({ body: jsonEncode({
"action": "receivable", "action": "receivable",
"account": destinationAddress, "account": destinationAddress,
@ -417,7 +427,7 @@ class NanoClient {
Future<List<NanoTransactionModel>> fetchTransactions(String address) async { Future<List<NanoTransactionModel>> fetchTransactions(String address) async {
try { try {
final response = await http.post(_node!.uri, final response = await http.post(_node!.uri,
headers: CAKE_HEADERS, headers: getHeaders(),
body: jsonEncode({ body: jsonEncode({
"action": "account_history", "action": "account_history",
"account": address, "account": address,

View file

@ -14,7 +14,7 @@ import 'package:nanodart/nanodart.dart';
import 'package:nanoutil/nanoutil.dart'; import 'package:nanoutil/nanoutil.dart';
class NanoWalletService extends WalletService<NanoNewWalletCredentials, class NanoWalletService extends WalletService<NanoNewWalletCredentials,
NanoRestoreWalletFromSeedCredentials, NanoRestoreWalletFromKeysCredentials> { NanoRestoreWalletFromSeedCredentials, NanoRestoreWalletFromKeysCredentials, NanoNewWalletCredentials> {
NanoWalletService(this.walletInfoSource); NanoWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -109,6 +109,11 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
return wallet; return wallet;
} }
@override
Future<NanoWallet> restoreFromHardwareWallet(NanoNewWalletCredentials credentials) {
throw UnimplementedError("Restoring a Nano wallet from a hardware wallet is not yet supported!");
}
@override @override
Future<NanoWallet> restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { Future<NanoWallet> restoreFromSeed(NanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async {
if (credentials.mnemonic.contains(' ')) { if (credentials.mnemonic.contains(' ')) {

View file

@ -173,10 +173,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.2" version: "1.17.1"
convert: convert:
dependency: transitive dependency: transitive
description: description:
@ -419,18 +419,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16" version: "0.12.15"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.0" version: "0.2.0"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@ -713,10 +713,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.0" version: "1.9.1"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -761,10 +761,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.0" version: "0.5.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@ -790,21 +790,13 @@ packages:
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
watcher: watcher:
dependency: "direct overridden" dependency: transitive
description: description:
name: watcher name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@ -838,5 +830,5 @@ packages:
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
sdks: sdks:
dart: ">=3.1.0-185.0.dev <4.0.0" dart: ">=3.0.0 <4.0.0"
flutter: ">=3.7.0" flutter: ">=3.7.0"

View file

@ -32,10 +32,13 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.1.11 build_runner: ^2.4.7
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -1,5 +1,6 @@
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart';
import 'package:cw_evm/evm_chain_wallet_service.dart'; import 'package:cw_evm/evm_chain_wallet_service.dart';
@ -86,6 +87,29 @@ class PolygonWalletService extends EVMChainWalletService<PolygonWallet> {
return wallet; return wallet;
} }
@override
Future<PolygonWallet> restoreFromHardwareWallet(
EVMChainRestoreWalletFromHardware credentials) async {
credentials.walletInfo!.derivationInfo = DerivationInfo(
derivationType: DerivationType.bip39,
derivationPath: "m/44'/60'/${credentials.hwAccountData.accountIndex}'/0/0"
);
credentials.walletInfo!.hardwareWalletType = credentials.hardwareWalletType;
credentials.walletInfo!.address = credentials.hwAccountData.address;
final wallet = PolygonWallet(
walletInfo: credentials.walletInfo!,
password: credentials.password!,
client: client,
);
await wallet.init();
wallet.addInitialTokens();
await wallet.save();
return wallet;
}
@override @override
Future<PolygonWallet> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials, Future<PolygonWallet> restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async { {bool? isTestnet}) async {

View file

@ -23,12 +23,19 @@ dependencies:
bip39: ^1.0.6 bip39: ^1.0.6
collection: ^1.17.1 collection: ^1.17.1
dependency_overrides:
web3dart:
git:
url: https://github.com/cake-tech/web3dart.git
ref: cake
watcher: ^1.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
build_runner: ^2.1.11 build_runner: ^2.4.7
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View file

@ -145,12 +145,17 @@ abstract class SolanaWalletBase
Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async { Future<Wallet> getWalletPair({String? mnemonic, String? privateKey}) async {
assert(mnemonic != null || privateKey != null); assert(mnemonic != null || privateKey != null);
if (privateKey != null) { if (mnemonic != null) {
final privateKeyBytes = base58decode(privateKey); return Wallet.fromMnemonic(mnemonic, account: 0, change: 0);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList());
} }
return Wallet.fromMnemonic(mnemonic!, account: 0, change: 0); try {
final privateKeyBytes = base58decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes.take(32).toList());
} catch (_) {
final privateKeyBytes = HEX.decode(privateKey!);
return await Wallet.fromPrivateKeyBytes(privateKey: privateKeyBytes);
}
} }
@override @override
@ -360,7 +365,7 @@ abstract class SolanaWalletBase
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': _mnemonic, 'mnemonic': _mnemonic,
'private_key': privateKey, 'private_key': _hexPrivateKey,
'balance': balance[currency]!.toJSON(), 'balance': balance[currency]!.toJSON(),
}); });
@ -519,7 +524,7 @@ abstract class SolanaWalletBase
_transactionsUpdateTimer!.cancel(); _transactionsUpdateTimer!.cancel();
} }
_transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 20), (_) { _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 30), (_) {
_updateBalance(); _updateBalance();
_updateNativeSOLTransactions(); _updateNativeSOLTransactions();
_updateSPLTokenTransactions(); _updateSPLTokenTransactions();

View file

@ -2,7 +2,10 @@ import 'dart:io';
import 'package:bip39/bip39.dart' as bip39; import 'package:bip39/bip39.dart' as bip39;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_service.dart';
@ -13,7 +16,7 @@ import 'package:cw_solana/solana_wallet_creation_credentials.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
class SolanaWalletService extends WalletService<SolanaNewWalletCredentials, class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey> { SolanaRestoreWalletFromSeedCredentials, SolanaRestoreWalletFromPrivateKey, SolanaNewWalletCredentials> {
SolanaWalletService(this.walletInfoSource); SolanaWalletService(this.walletInfoSource);
final Box<WalletInfo> walletInfoSource; final Box<WalletInfo> walletInfoSource;
@ -134,4 +137,10 @@ class SolanaWalletService extends WalletService<SolanaNewWalletCredentials,
await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); await walletInfoSource.put(currentWalletInfo.key, newWalletInfo);
} }
@override
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(SolanaNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
throw UnimplementedError();
}
} }

View file

@ -26,10 +26,13 @@ dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
build_runner: ^2.1.11 build_runner: ^2.4.7
mobx_codegen: ^2.0.7 mobx_codegen: ^2.0.7
hive_generator: ^1.1.3 hive_generator: ^1.1.3
dependency_overrides:
watcher: ^1.1.0
flutter: flutter:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg

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: false,
),
TronToken(
name: "Ethereum",
symbol: "ETH",
contractAddress: "TRFe3hT5oYhjSZ6f3ji5FJ7YCfrkWnHRvh",
decimal: 18,
enabled: false,
),
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,563 @@
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');
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!;
}
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().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 = 269000;
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().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) {
final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up.\nTransaction fee: $feeInTrx TRX',
);
}
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}");
throw Exception(
'An error occured while creating the transfer request. Please try again.',
);
}
final feeLimit = await getFeeLimit(
request.transactionRaw!,
ownerAddress,
receiverAddress,
energyUsed: request.energyUsed ?? 0,
);
final tronBalanceInt = tronBalance.toInt();
if (feeLimit > tronBalanceInt) {
final feeInTrx = TronHelper.fromSun(BigInt.parse(feeLimit.toString()));
throw Exception(
'You don\'t have enough TRX to cover the transaction fee for this transaction. Please top up. Transaction fee: $feeInTrx TRX',
);
}
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',
if (url.contains("trongrid")) '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',
if (url.contains("trongrid")) '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,566 @@
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';
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());
}
}
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 final TronClient _client;
Timer? _transactionsUpdateTimer;
@override
WalletAddresses walletAddresses;
@observable
String? nativeTxEstimatedFee;
@observable
String? trc20EstimatedFee;
@override
@observable
SyncStatus syncStatus;
@override
@observable
late ObservableMap<CryptoCurrency, TronBalance> balance;
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 (_) {}
}
}
@override
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: 30), (_) async {
_updateBalance();
await fetchTransactions();
fetchTrc20ExcludedTransactions();
});
}
@override
Future<String> signMessage(String message, {String? address}) async =>
_tronPrivateKey.signPersonalMessage(ascii.encode(message));
String getTronBase58AddressFromHex(String hexAddress) {
return TronAddress(hexAddress).toAddress();
}
void updateScanProviderUsageState(bool isEnabled) {
if (isEnabled) {
fetchTransactions();
fetchTrc20ExcludedTransactions();
_setTransactionUpdateTimer();
} else {
_transactionsUpdateTimer?.cancel();
}
}
}

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,160 @@
import 'dart:io';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_core/balance.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.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,
TronNewWalletCredentials> {
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);
}
@override
Future<WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>> restoreFromHardwareWallet(TronNewWalletCredentials credentials) {
// TODO: implement restoreFromHardwareWallet
throw UnimplementedError();
}
}

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,301 @@
# 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`
- and if it has tokens (ex. erc20, trc20, spl tokens) then add them to the switch case as well
- 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`
1. if it has tokens (ex. ERC20, SPL, etc...) make sure to add a check for the tags as well in the
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)
Copyright (C) 2018-2023 Cake Labs LLC

View file

@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>11.0</string> <string>12.0</string>
</dict> </dict>
</plist> </plist>

View file

@ -109,6 +109,8 @@ PODS:
- flutter_inappwebview_ios/Core (0.0.1): - flutter_inappwebview_ios/Core (0.0.1):
- Flutter - Flutter
- OrderedSet (~> 5.0) - OrderedSet (~> 5.0)
- flutter_local_authentication (1.2.0):
- Flutter
- flutter_mailer (0.0.1): - flutter_mailer (0.0.1):
- Flutter - Flutter
- flutter_secure_storage (6.0.0): - flutter_secure_storage (6.0.0):
@ -118,8 +120,6 @@ PODS:
- Toast - Toast
- in_app_review (0.2.0): - in_app_review (0.2.0):
- Flutter - Flutter
- local_auth_ios (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11) - MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- package_info (0.0.1): - package_info (0.0.1):
@ -131,7 +131,12 @@ PODS:
- FlutterMacOS - FlutterMacOS
- permission_handler_apple (9.1.1): - permission_handler_apple (9.1.1):
- Flutter - Flutter
- Protobuf (3.25.3)
- ReachabilitySwift (5.0.0) - ReachabilitySwift (5.0.0)
- reactive_ble_mobile (0.0.1):
- Flutter
- Protobuf (~> 3.5)
- SwiftProtobuf (~> 1.0)
- SDWebImage (5.18.11): - SDWebImage (5.18.11):
- SDWebImage/Core (= 5.18.11) - SDWebImage/Core (= 5.18.11)
- SDWebImage/Core (5.18.11) - SDWebImage/Core (5.18.11)
@ -170,15 +175,16 @@ DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`) - package_info (from `.symlinks/plugins/package_info/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- reactive_ble_mobile (from `.symlinks/plugins/reactive_ble_mobile/ios`)
- sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@ -196,6 +202,7 @@ SPEC REPOS:
- DKPhotoGallery - DKPhotoGallery
- MTBBarcodeScanner - MTBBarcodeScanner
- OrderedSet - OrderedSet
- Protobuf
- ReachabilitySwift - ReachabilitySwift
- SDWebImage - SDWebImage
- SwiftProtobuf - SwiftProtobuf
@ -226,6 +233,8 @@ EXTERNAL SOURCES:
:path: Flutter :path: Flutter
flutter_inappwebview_ios: flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios" :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_authentication:
:path: ".symlinks/plugins/flutter_local_authentication/ios"
flutter_mailer: flutter_mailer:
:path: ".symlinks/plugins/flutter_mailer/ios" :path: ".symlinks/plugins/flutter_mailer/ios"
flutter_secure_storage: flutter_secure_storage:
@ -234,8 +243,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
in_app_review: in_app_review:
:path: ".symlinks/plugins/in_app_review/ios" :path: ".symlinks/plugins/in_app_review/ios"
local_auth_ios:
:path: ".symlinks/plugins/local_auth_ios/ios"
package_info: package_info:
:path: ".symlinks/plugins/package_info/ios" :path: ".symlinks/plugins/package_info/ios"
package_info_plus: package_info_plus:
@ -244,6 +251,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin" :path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
reactive_ble_mobile:
:path: ".symlinks/plugins/reactive_ble_mobile/ios"
sensitive_clipboard: sensitive_clipboard:
:path: ".symlinks/plugins/sensitive_clipboard/ios" :path: ".symlinks/plugins/sensitive_clipboard/ios"
share_plus: share_plus:
@ -273,30 +282,32 @@ SPEC CHECKSUMS:
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
Protobuf: 8e9074797a13c484a79959fdb819ef4ae6da7dbe
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
reactive_ble_mobile: 9ce6723d37ccf701dbffd202d487f23f5de03b4c
SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457 SDWebImage: a3ba0b8faac7228c3c8eadd1a55c9c9fe5e16457
sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
Toast: ec33c32b8688982cecc6348adeae667c1b9938da Toast: ec33c32b8688982cecc6348adeae667c1b9938da
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6

View file

@ -16,6 +16,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -40,6 +41,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; AD0937B0140D5A4C24E73BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -123,6 +125,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */, 97C147021CF9000F007C117D /* Info.plist */,
9F46EE5D2BC11178009318F5 /* PrivacyInfo.xcprivacy */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@ -196,6 +199,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
9F46EE5E2BC11178009318F5 /* PrivacyInfo.xcprivacy in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

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