mirror of https://github.com/CympleTech/ESSE.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1264 lines
44 KiB
1264 lines
44 KiB
import 'package:flutter/material.dart'; |
|
import 'package:flutter/services.dart'; |
|
import 'package:provider/provider.dart'; |
|
import 'package:url_launcher/url_launcher.dart'; |
|
import 'package:qr_flutter/qr_flutter.dart'; |
|
|
|
import 'package:esse/utils/adaptive.dart'; |
|
import 'package:esse/utils/better_print.dart'; |
|
import 'package:esse/l10n/localizations.dart'; |
|
import 'package:esse/widgets/show_pin.dart'; |
|
import 'package:esse/widgets/button_text.dart'; |
|
import 'package:esse/widgets/input_text.dart'; |
|
import 'package:esse/widgets/shadow_dialog.dart'; |
|
import 'package:esse/widgets/default_core_show.dart'; |
|
import 'package:esse/provider.dart'; |
|
import 'package:esse/options.dart'; |
|
import 'package:esse/rpc.dart'; |
|
|
|
import 'package:esse/apps/wallet/models.dart'; |
|
|
|
class WalletDetail extends StatefulWidget { |
|
const WalletDetail({Key? key}) : super(key: key); |
|
|
|
@override |
|
_WalletDetailState createState() => _WalletDetailState(); |
|
} |
|
|
|
class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderStateMixin { |
|
TabController? _tabController; |
|
bool _needGenerate = false; |
|
|
|
List<Address> _addresses = []; |
|
Address? _selectedAddress; |
|
|
|
List<Network> _networks = []; |
|
Network? _selectedNetwork; |
|
|
|
Token _mainToken = Token(); |
|
List<Token> _tokens = []; |
|
List<Transaction> _txs = []; |
|
|
|
@override |
|
void initState() { |
|
_tabController = new TabController(length: 2, vsync: this); |
|
|
|
rpc.addListener('wallet-generate', _walletGenerate); |
|
rpc.addListener('wallet-import', _walletGenerate); |
|
rpc.addListener('wallet-token', _walletToken); |
|
rpc.addListener('wallet-balance', _walletBalance); |
|
rpc.addListener('wallet-transfer', _walletTransfer); |
|
|
|
super.initState(); |
|
Future.delayed(Duration.zero, _load); |
|
} |
|
|
|
_walletGenerate(List params) { |
|
final address = Address.fromList(params); |
|
bool isNew = true; |
|
this._addresses.forEach((addr) { |
|
if (addr.address == address.address) { |
|
isNew = false; |
|
} |
|
}); |
|
if (isNew) { |
|
this._addresses.add(address); |
|
_changeAddress(address); |
|
setState(() {}); |
|
} |
|
} |
|
|
|
_walletToken(List params) { |
|
final network = NetworkExtension.fromInt(params[0]); |
|
if (network == this._selectedNetwork!) { |
|
this._tokens.clear(); |
|
params[1].forEach((param) { |
|
this._tokens.add(Token.fromList(param, '0')); |
|
}); |
|
} |
|
setState(() {}); |
|
} |
|
|
|
_walletBalance(List params) { |
|
final address = params[0]; |
|
final network = NetworkExtension.fromInt(params[1]); |
|
if (address == this._selectedAddress!.address && network == this._selectedNetwork!) { |
|
final balance = params[2]; |
|
|
|
if (params.length == 4) { |
|
final token = Token.fromList(params[3], balance); |
|
bool isNew = true; |
|
int key = 0; |
|
this._tokens.asMap().forEach((k, t) { |
|
if (t.contract == token.contract) { |
|
isNew = false; |
|
key = k; |
|
} |
|
}); |
|
if (isNew) { |
|
this._tokens.add(token); |
|
} else { |
|
this._tokens[key].updateBalance(balance); |
|
} |
|
} else { |
|
this._mainToken = Token.eth(network); |
|
this._mainToken.updateBalance(balance); |
|
} |
|
setState(() {}); |
|
} |
|
} |
|
|
|
_walletTransfer(List params) { |
|
final addressId = params[0]; |
|
final network = NetworkExtension.fromInt(params[1]); |
|
final tx = Transaction.fromList(params[2]); |
|
|
|
bool isNew = true; |
|
this._txs.asMap().forEach((k, t) { |
|
if (t.hash == tx.hash) { |
|
isNew = false; |
|
} |
|
}); |
|
if (isNew) { |
|
this._txs.add(tx); |
|
this._tabController!.animateTo(1); |
|
} |
|
} |
|
|
|
_load() async { |
|
final res = await httpPost('wallet-list', []); |
|
if (res.isOk) { |
|
this._addresses.clear(); |
|
res.params.forEach((param) { |
|
final address = Address.fromList(param); |
|
this._addresses.add(address); |
|
if (address.isMain) { |
|
_changeAddress(address); |
|
} |
|
}); |
|
if (this._addresses.length == 0) { |
|
this._needGenerate = true; |
|
} |
|
setState(() {}); |
|
} else { |
|
// TODO tostor error |
|
print(res.error); |
|
} |
|
} |
|
|
|
_changeAddress(Address address) { |
|
this._selectedAddress = address; |
|
this._networks = address.networks(); |
|
this._txs.clear(); |
|
if (!this._networks.contains(this._selectedNetwork)) { |
|
_changeNetwork(this._networks[0]); |
|
} else { |
|
rpc.send('wallet-token', [ |
|
this._selectedNetwork!.toInt(), this._selectedAddress!.address |
|
]); |
|
} |
|
this._mainToken = address.mainToken(this._selectedNetwork!); |
|
} |
|
|
|
_changeNetwork(Network network) { |
|
this._selectedNetwork = network; |
|
this._txs.clear(); |
|
rpc.send('wallet-token', [ |
|
this._selectedNetwork!.toInt(), this._selectedAddress!.address |
|
]); |
|
} |
|
|
|
_setMain(int id) { |
|
rpc.send('wallet-main', [id]); |
|
for (int i=0;i<this._addresses.length;i++) { |
|
if (this._addresses[i].id == id) { |
|
this._addresses[i].isMain = true; |
|
} else { |
|
this._addresses[i].isMain = false; |
|
} |
|
} |
|
Navigator.pop(context); |
|
setState(() {}); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final color = Theme.of(context).colorScheme; |
|
final lang = AppLocalizations.of(context); |
|
|
|
if (this._addresses.length == 0 && !this._needGenerate) { |
|
return Scaffold( |
|
appBar: AppBar(title: Text(lang.loadMore)), |
|
body: const DefaultCoreShow(), |
|
); |
|
} |
|
|
|
if (this._addresses.length == 0 && this._needGenerate) { |
|
return Scaffold( |
|
appBar: AppBar(title: Text(lang.wallet)), |
|
body: DefaultCoreShow( |
|
child: ElevatedButton( |
|
style: ElevatedButton.styleFrom(onPrimary: color.surface), |
|
onPressed: () { |
|
final pin = context.read<AccountProvider>().pin; |
|
rpc.send('wallet-generate', [ChainToken.ETH.toInt(), pin]); |
|
}, |
|
child: Padding( |
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), |
|
child: Row( |
|
mainAxisSize: MainAxisSize.min, |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
const Icon(Icons.lock), |
|
const SizedBox(width: 8.0), |
|
Text(lang.walletGenerate), |
|
] |
|
) |
|
) |
|
)) |
|
); |
|
} |
|
|
|
if (this._selectedAddress == null) { |
|
_changeAddress(this._addresses[0]); |
|
} |
|
|
|
List<PopupMenuEntry<int>> addressWidges = []; |
|
this._addresses.asMap().forEach((index, value) { |
|
addressWidges.add(_menuItem(index + 3, value, color, value == this._selectedAddress, lang)); |
|
}); |
|
|
|
final max = MediaQuery.of(context).size.height; |
|
double maxHeight = max - 360; |
|
if (maxHeight < 200) { |
|
maxHeight = 200; |
|
} |
|
|
|
return Scaffold( |
|
appBar: AppBar( |
|
title: DropdownButton<Network>( |
|
icon: Container(), |
|
underline: Container(), |
|
value: this._selectedNetwork, |
|
onChanged: (Network? value) { |
|
if (value != null) { |
|
setState(() { |
|
_changeNetwork(value); |
|
}); |
|
} |
|
}, |
|
items: this._networks.map((Network network) { |
|
final params = network.params(); |
|
return DropdownMenuItem<Network>( |
|
value: network, |
|
child: Container( |
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), |
|
decoration: BoxDecoration( |
|
border: Border.all(width: 1.0, color: params[1]), |
|
borderRadius: BorderRadius.circular(25.0) |
|
), |
|
child: Row( |
|
children: <Widget>[ |
|
Icon(Icons.public, color: params[1], size: 18.0), |
|
const SizedBox(width: 10), |
|
Text(params[0], style: TextStyle(color: params[1], fontSize: 14.0)), |
|
], |
|
)), |
|
); |
|
}).toList(), |
|
), |
|
actions: [ |
|
Padding( |
|
padding: const EdgeInsets.symmetric(horizontal: 20.0), |
|
child: PopupMenuButton<int>( |
|
child: Container( |
|
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), |
|
width: 40.0, |
|
decoration: BoxDecoration( |
|
color: color.surface, |
|
borderRadius: BorderRadius.circular(25.0) |
|
), |
|
child: Center(child: Text(this._selectedAddress!.icon())) |
|
), |
|
onSelected: (int value) { |
|
if (value == 0) { |
|
rpc.send('wallet-generate', [this._selectedAddress!.chain.toInt(), ""]); |
|
} else if (value == 1) { |
|
showShadowDialog(context, Icons.vertical_align_bottom, lang.importAccount, |
|
_ImportAccount(chain: this._selectedAddress!.chain), 20.0 |
|
); |
|
} else if (value == 2) { |
|
// |
|
} else { |
|
setState(() { |
|
_changeAddress(this._addresses[value - 3]); |
|
}); |
|
} |
|
}, |
|
itemBuilder: (context) { |
|
return addressWidges + <PopupMenuEntry<int>>[ |
|
PopupMenuItem<int>( |
|
value: 0, |
|
child: Column( |
|
children: [ |
|
const Divider(), |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
children: [ |
|
const Icon(Icons.add, color: Color(0xFF6174FF)), |
|
Text(lang.createAccount, style: TextStyle(color: const Color(0xFF6174FF))), |
|
] |
|
), |
|
] |
|
) |
|
), |
|
PopupMenuItem<int>( |
|
value: 1, |
|
child: Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
children: [ |
|
const Icon(Icons.vertical_align_bottom, color: Color(0xFF6174FF)), |
|
Text(lang.importAccount, style: TextStyle(color: const Color(0xFF6174FF))), |
|
] |
|
), |
|
), |
|
PopupMenuItem<int>( |
|
value: 2, |
|
child: Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
children: [ |
|
const Icon(Icons.settings, color: Color(0xFF6174FF)), |
|
Text(lang.setting, style: TextStyle(color: const Color(0xFF6174FF))), |
|
] |
|
), |
|
) |
|
]; |
|
}, |
|
), |
|
) |
|
] |
|
), |
|
body: SingleChildScrollView( |
|
child: Container( |
|
alignment: Alignment.topCenter, |
|
padding: const EdgeInsets.symmetric(horizontal: 20.0), |
|
child: Column( |
|
children:[ |
|
InkWell( |
|
onTap: () { |
|
Clipboard.setData(ClipboardData(text: this._selectedAddress!.address)); |
|
}, |
|
child: Container( |
|
padding: const EdgeInsets.symmetric(vertical: 10.0), |
|
alignment: Alignment.center, |
|
decoration: new BoxDecoration( |
|
border: new Border(bottom: |
|
const BorderSide(width: 1.0, color: Color(0xA0ADB0BB)))), |
|
child: Column( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Text(this._selectedAddress!.name, style: TextStyle(fontSize: 18.0)), |
|
const SizedBox(height: 4.0), |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Text(this._selectedAddress!.short(), |
|
style: TextStyle(color: Color(0xFFADB0BB))), |
|
const SizedBox(width: 8.0), |
|
Icon(Icons.copy, size: 16.0, color: color.primary), |
|
] |
|
) |
|
] |
|
), |
|
), |
|
), |
|
Container( |
|
padding: const EdgeInsets.symmetric(vertical: 20.0), |
|
child: Column( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Container( |
|
width: 36.0, |
|
height: 36.0, |
|
decoration: BoxDecoration( |
|
image: DecorationImage( |
|
image: AssetImage(this._mainToken.logo), |
|
fit: BoxFit.cover, |
|
), |
|
), |
|
), |
|
Container( |
|
height: 60.0, |
|
alignment: Alignment.center, |
|
child: Text( |
|
"${this._mainToken.amount} ${this._mainToken.name}", |
|
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)), |
|
), |
|
//Text('\$0.0', style: TextStyle(color: Color(0xFFADB0BB))), |
|
const SizedBox(height: 8.0), |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
children: [ |
|
TextButton( |
|
onPressed: () => showShadowDialog( |
|
context, Icons.input, this._mainToken.name, _TransferToken( |
|
chain: this._selectedAddress!.chain, |
|
network: this._selectedNetwork!, |
|
address: this._selectedAddress!, |
|
token: this._mainToken, |
|
addresses: this._addresses, |
|
), 0.0 |
|
), |
|
child: Container( |
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), |
|
decoration: BoxDecoration( |
|
color: Color(0xFF6174FF), |
|
borderRadius: BorderRadius.circular(25.0) |
|
), |
|
child: Center(child: |
|
Row( |
|
children: [ |
|
Icon(Icons.input, color: Colors.white, size: 18.0), |
|
const SizedBox(width: 10.0), |
|
Text(lang.send, style: TextStyle(color: Colors.white)) |
|
] |
|
) |
|
) |
|
) |
|
), |
|
TextButton( |
|
onPressed: () { |
|
showShadowDialog(context, Icons.qr_code, lang.receive, |
|
Column( |
|
children: [ |
|
Container( |
|
width: 200.0, |
|
padding: const EdgeInsets.all(2.0), |
|
margin: const EdgeInsets.only(bottom: 20.0), |
|
decoration: BoxDecoration( |
|
borderRadius: BorderRadius.circular(5.0), |
|
border: Border.all(color: Color(0x40ADB0BB)), |
|
color: Colors.white, |
|
), |
|
child: Center( |
|
child: QrImage( |
|
data: this._selectedAddress!.address, |
|
version: QrVersions.auto, |
|
foregroundColor: Colors.black, |
|
), |
|
), |
|
), |
|
Text(this._selectedAddress!.address) |
|
] |
|
)); |
|
}, |
|
child: Container( |
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10.0), |
|
decoration: BoxDecoration( |
|
color: Color(0xFF6174FF), |
|
borderRadius: BorderRadius.circular(25.0) |
|
), |
|
child: Center(child: |
|
Row( |
|
children: [ |
|
Icon(Icons.qr_code, color: Colors.white, size: 18.0), |
|
const SizedBox(width: 10.0), |
|
Text(lang.receive, style: TextStyle(color: Colors.white)) |
|
] |
|
) |
|
) |
|
) |
|
), |
|
] |
|
), |
|
] |
|
) |
|
), |
|
TabBar( |
|
unselectedLabelColor: color.onSurface, |
|
labelColor: Color(0xFF6174FF), |
|
tabs: [ |
|
Tab(text: 'Tokens'), |
|
Tab(text: 'Activity'), |
|
], |
|
controller: _tabController!, |
|
indicatorSize: TabBarIndicatorSize.tab, |
|
), |
|
Container( |
|
height: maxHeight, |
|
child: TabBarView( |
|
children: [ |
|
ListView.separated( |
|
separatorBuilder: (BuildContext context, int index) => const Divider(), |
|
itemCount: this._tokens.length + 1, |
|
itemBuilder: (BuildContext context, int index) { |
|
if (index == this._tokens.length) { |
|
return TextButton( |
|
child: Padding( |
|
padding: const EdgeInsets.symmetric(vertical: 10.0), |
|
child: Text('Add Token' + ' ( ERC20 / ERC721 )') |
|
), |
|
onPressed: () => showShadowDialog( |
|
context, Icons.paid, 'Token', _ImportToken( |
|
chain: this._selectedAddress!.chain, |
|
network: this._selectedNetwork!, |
|
address: this._selectedAddress!.address |
|
), 10.0 |
|
), |
|
); |
|
} else { |
|
final token = this._tokens[index]; |
|
return ListTile( |
|
leading: Container( |
|
width: 36.0, |
|
height: 36.0, |
|
decoration: BoxDecoration( |
|
image: DecorationImage( |
|
image: AssetImage(token.logo), |
|
fit: BoxFit.cover, |
|
), |
|
), |
|
), |
|
title: Text("${token.balance} ${token.name}",), |
|
subtitle: Text(token.short()), |
|
trailing: Row( |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
if (token.isNft()) |
|
IconButton(icon: Icon(Icons.travel_explore, color: color.primary), |
|
onPressed: () => showShadowDialog( |
|
context, Icons.travel_explore, token.name, _ImportNft( |
|
address: this._selectedAddress!, |
|
token: token, |
|
), 0.0 |
|
), |
|
), |
|
IconButton(icon: Icon(Icons.input, color: color.primary), |
|
onPressed: () => showShadowDialog( |
|
context, Icons.input, token.name, _TransferToken( |
|
chain: this._selectedAddress!.chain, |
|
network: this._selectedNetwork!, |
|
address: this._selectedAddress!, |
|
token: token, |
|
addresses: this._addresses, |
|
), 0.0 |
|
), |
|
), |
|
] |
|
) |
|
); |
|
} |
|
} |
|
), |
|
ListView.separated( |
|
separatorBuilder: (BuildContext context, int index) => const Divider(), |
|
itemCount: this._txs.length + 1, |
|
itemBuilder: (BuildContext context, int index) { |
|
if (index == this._txs.length) { |
|
return SizedBox(); |
|
// return TextButton( |
|
// child: Padding( |
|
// padding: const EdgeInsets.symmetric(vertical: 10.0), |
|
// child: Text(lang.loadMore) |
|
// ), |
|
// onPressed: () { |
|
// // |
|
// } |
|
// ); |
|
} else { |
|
final tx = this._txs[index]; |
|
return ListTile( |
|
title: Text('Hash: ' + tx.short_hash()), |
|
subtitle: Text('To: ' + tx.short_to()), |
|
trailing: IconButton(icon: Icon(Icons.link, color: color.primary), |
|
onPressed: () { |
|
launch(this._selectedNetwork!.txUrl() + tx.hash); |
|
} |
|
), |
|
); |
|
} |
|
} |
|
), |
|
], |
|
controller: _tabController!, |
|
), |
|
), |
|
] |
|
) |
|
))); |
|
} |
|
|
|
PopupMenuEntry<int> _menuItem(int value, Address address, ColorScheme color, bool selected, lang) { |
|
return PopupMenuItem<int>( |
|
value: value, |
|
child: Row( |
|
children: [ |
|
Icon(Icons.check, color: selected ? color.onSurface : Colors.transparent), |
|
const SizedBox(width: 10.0), |
|
Expanded( |
|
child: Column( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
children: [ |
|
Text(address.name), |
|
const SizedBox(height: 4.0), |
|
Text( |
|
address.balance(this._selectedNetwork!) + ' ' + address.chain.symbol, |
|
style: TextStyle(fontSize: 14.0, color: Color(0xFFADB0BB)) |
|
), |
|
] |
|
), |
|
), |
|
InkWell( |
|
onTap: address.isMain ? null : () => _setMain(address.id), |
|
child: Container( |
|
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), |
|
decoration: address.isMain |
|
? BoxDecoration() |
|
: BoxDecoration( |
|
color: Color(0x266174FF), |
|
borderRadius: BorderRadius.circular(15.0) |
|
), |
|
child: Text(address.isMain ? lang.main : lang.setMain, |
|
style: TextStyle(fontSize: 14.0)) |
|
) |
|
) |
|
] |
|
), |
|
); |
|
} |
|
} |
|
|
|
class _ImportAccount extends StatelessWidget { |
|
final ChainToken chain; |
|
TextEditingController _nameController = TextEditingController(); |
|
FocusNode _nameFocus = FocusNode(); |
|
|
|
_ImportAccount({Key? key, required this.chain}) : super(key: key); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final color = Theme.of(context).colorScheme; |
|
final lang = AppLocalizations.of(context); |
|
_nameFocus.requestFocus(); |
|
|
|
return Column( |
|
children: [ |
|
Container( |
|
padding: EdgeInsets.only(bottom: 20.0), |
|
child: InputText( |
|
icon: Icons.vpn_key, |
|
text: lang.secretKey, |
|
controller: _nameController, |
|
focus: _nameFocus), |
|
), |
|
ButtonText( |
|
text: lang.send, |
|
action: () { |
|
final secret = _nameController.text.trim(); |
|
if (secret.length < 32) { |
|
return; |
|
} |
|
final pin = context.read<AccountProvider>().pin; |
|
rpc.send('wallet-import', [chain.toInt(), secret, pin]); |
|
Navigator.pop(context); |
|
}), |
|
] |
|
); |
|
} |
|
} |
|
|
|
class _ImportToken extends StatefulWidget { |
|
final ChainToken chain; |
|
final Network network; |
|
final String address; |
|
_ImportToken({Key? key, required this.chain, required this.network, required this.address}) : super(key: key); |
|
|
|
@override |
|
_ImportTokenState createState() => _ImportTokenState(); |
|
} |
|
|
|
class _ImportTokenState extends State<_ImportToken> { |
|
TextEditingController _nameController = TextEditingController(); |
|
FocusNode _nameFocus = FocusNode(); |
|
ChainToken _selectedChain = ChainToken.ERC20; |
|
|
|
Widget _chain(ChainToken value, String show, color) { |
|
return Row( |
|
mainAxisSize: MainAxisSize.min, |
|
children: [ |
|
Radio( |
|
value: value, |
|
groupValue: _selectedChain, |
|
onChanged: (ChainToken? n) => setState(() { |
|
if (n != null) { |
|
_selectedChain = n; |
|
} |
|
})), |
|
_selectedChain == value |
|
? Text(show, style: TextStyle(color: color.primary)) |
|
: Text(show), |
|
] |
|
); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final color = Theme.of(context).colorScheme; |
|
final lang = AppLocalizations.of(context); |
|
final params = widget.network.params(); |
|
_nameFocus.requestFocus(); |
|
|
|
return Column( |
|
children: [ |
|
Text(params[0], style: TextStyle(color: params[1], fontWeight: FontWeight.bold)), |
|
const SizedBox(height: 20.0), |
|
if (widget.chain.isEth()) |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, |
|
children: [ |
|
_chain(ChainToken.ERC20, "ERC20", color), |
|
_chain(ChainToken.ERC721, "ERC721", color), |
|
] |
|
), |
|
const SizedBox(height: 10.0), |
|
Container( |
|
padding: EdgeInsets.only(bottom: 20.0), |
|
child: InputText( |
|
icon: Icons.location_on, |
|
text: lang.contract + ' (0x...)', |
|
controller: _nameController, |
|
focus: _nameFocus), |
|
), |
|
ButtonText( |
|
text: lang.send, |
|
action: () { |
|
final contract = _nameController.text.trim(); |
|
if (contract.length < 20) { |
|
return; |
|
} |
|
rpc.send('wallet-token-import', [ |
|
_selectedChain.toInt(), widget.network.toInt(), widget.address, contract |
|
]); |
|
Navigator.pop(context); |
|
}), |
|
] |
|
); |
|
} |
|
} |
|
|
|
class _TransferToken extends StatefulWidget { |
|
final ChainToken chain; |
|
final Network network; |
|
final Address address; |
|
final Token token; |
|
final List addresses; |
|
_TransferToken({Key? key, required this.chain, required this.network, required this.address, required this.token, required this.addresses}) : super(key: key); |
|
|
|
@override |
|
_TransferTokenState createState() => _TransferTokenState(); |
|
} |
|
|
|
class _TransferTokenState extends State<_TransferToken> { |
|
TextEditingController _nameController = TextEditingController(); |
|
FocusNode _nameFocus = FocusNode(); |
|
TextEditingController _amountController = TextEditingController(); |
|
FocusNode _amountFocus = FocusNode(); |
|
|
|
bool _myAccount = false; |
|
String _selectAddress = ''; |
|
|
|
String _price = ''; |
|
String _gas = '0'; |
|
String _networkError = ''; |
|
bool _checked = false; |
|
bool _checking = false; |
|
|
|
List<String> _nft = []; |
|
String _selectNft = '-'; |
|
bool _nftInput = false; |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
_nameController.addListener(() { |
|
setState(() { |
|
this._checked = false; |
|
this._checking = false; |
|
}); |
|
}); |
|
_amountController.addListener(() { |
|
setState(() { |
|
this._checked = false; |
|
this._checking = false; |
|
}); |
|
}); |
|
if (widget.token.isNft()) { |
|
_getNft(); |
|
} |
|
} |
|
|
|
_getNft() async { |
|
final res = await httpPost('wallet-nft', [widget.address.id, widget.token.id]); |
|
if (res.isOk) { |
|
final a = res.params[0]; |
|
final t = res.params[1]; |
|
if (a == widget.address.id && t == widget.token.id) { |
|
this._nft.clear(); |
|
res.params[2].forEach((hash) { |
|
this._nft.add(hash); |
|
}); |
|
} |
|
if (this._nft.length > 0) { |
|
this._selectNft = this._nft[0]; |
|
this._nftInput = false; |
|
} else { |
|
this._nftInput = true; |
|
} |
|
} else { |
|
this._nftInput = true; |
|
} |
|
setState(() {}); |
|
} |
|
|
|
_gasPrice(String to, String amount) async { |
|
final res = await httpPost('wallet-gas-price', [ |
|
widget.token.chain.toInt(), widget.network.toInt(), |
|
widget.address.address, to, amount, |
|
widget.token.contract |
|
]); |
|
if (res.isOk) { |
|
this._price = unitBalance(res.params[0], 9, 0); |
|
this._gas = unitBalance(res.params[1], 18, 6); |
|
this._networkError = ''; |
|
this._checked = true; |
|
} else { |
|
this._networkError = res.error; |
|
} |
|
this._checking = false; |
|
setState(() {}); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final color = Theme.of(context).colorScheme; |
|
final lang = AppLocalizations.of(context); |
|
final params = widget.network.params(); |
|
|
|
return Column( |
|
children: [ |
|
Text(params[0], style: TextStyle(color: params[1], fontWeight: FontWeight.bold)), |
|
const SizedBox(height: 20.0), |
|
Container( |
|
margin: const EdgeInsets.only(bottom: 5.0), |
|
padding: const EdgeInsets.all(15.0), |
|
decoration: BoxDecoration( |
|
color: Color(0x266174FF), |
|
borderRadius: BorderRadius.circular(10.0) |
|
), |
|
child: Column( |
|
children: [ |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
RichText( |
|
text: TextSpan( |
|
children: <TextSpan>[ |
|
TextSpan( |
|
text: widget.address.name, |
|
style: TextStyle(fontWeight: FontWeight.bold, color: color.primary) |
|
), |
|
TextSpan(text: ' (' + widget.address.short() + ')', style: TextStyle( |
|
fontSize: 14.0, fontStyle: FontStyle.italic, color: color.onSurface)), |
|
], |
|
), |
|
) |
|
] |
|
), |
|
const SizedBox(height: 20.0), |
|
Text("${widget.token.balance} ${widget.token.name}", |
|
style: TextStyle(fontWeight: FontWeight.bold)), |
|
const SizedBox(height: 10.0), |
|
if (this._checked) |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.center, |
|
children: [ |
|
Icon(Icons.arrow_forward, color: Colors.green), |
|
const SizedBox(width: 10.0), |
|
Expanded( |
|
child: this._networkError.length > 1 |
|
? Text(this._networkError, |
|
textAlign: TextAlign.center, style: TextStyle(color: Colors.red)) |
|
: RichText( |
|
textAlign: TextAlign.center, |
|
text: TextSpan( |
|
text: 'Estimated Price = ', |
|
style: TextStyle( |
|
fontSize: 14.0, fontStyle: FontStyle.italic, color: Colors.green), |
|
children: <TextSpan>[ |
|
TextSpan(text: this._price + ' Gwei', |
|
style: TextStyle(fontWeight: FontWeight.bold)), |
|
TextSpan(text: ', Gas ≈ '), |
|
TextSpan(text: this._gas + ' ETH', |
|
style: TextStyle(fontWeight: FontWeight.bold)), |
|
], |
|
), |
|
) |
|
), |
|
] |
|
) |
|
] |
|
)), |
|
this._myAccount |
|
? Container( |
|
margin: const EdgeInsets.only(top: 22.0, bottom: 5.0), |
|
padding: const EdgeInsets.symmetric(horizontal: 20.0), |
|
decoration: BoxDecoration( |
|
color: color.surface, |
|
borderRadius: BorderRadius.circular(10.0) |
|
), |
|
child: DropdownButtonHideUnderline( |
|
child: Theme( |
|
data: Theme.of(context).copyWith( |
|
canvasColor: color.background, |
|
), |
|
child: DropdownButton<String>( |
|
iconEnabledColor: Color(0xFFADB0BB), |
|
isExpanded: true, |
|
value: this._selectAddress, |
|
onChanged: (String? addr) { |
|
if (addr != null) { |
|
setState(() { |
|
this._checked = false; |
|
this._selectAddress = addr; |
|
}); |
|
} |
|
}, |
|
items: widget.addresses.map((address) { |
|
return DropdownMenuItem<String>( |
|
value: address.address, |
|
child: Row( |
|
children: [ |
|
Expanded( |
|
child: Text("${address.name}", |
|
maxLines: 1, |
|
overflow: TextOverflow.ellipsis, |
|
style: TextStyle(fontSize: 16) |
|
), |
|
), |
|
Text(" (${address.short()})", style: TextStyle(fontSize: 16)), |
|
const SizedBox(width: 10.0), |
|
] |
|
), |
|
); |
|
}).toList(), |
|
), |
|
), |
|
) |
|
) |
|
: Container( |
|
padding: const EdgeInsets.only(top: 20.0, bottom: 5.0), |
|
child: Row( |
|
children: [ |
|
Expanded( |
|
child: InputText( |
|
icon: Icons.person, |
|
text: lang.account.toLowerCase() + ' (0x...)', |
|
controller: _nameController, |
|
focus: _nameFocus |
|
)), |
|
SizedBox(width: 80.0, |
|
child: IconButton( |
|
icon: Icon(Icons.qr_code, color: color.primary), |
|
onPressed: () { |
|
// TODO |
|
} |
|
) |
|
) |
|
]) |
|
), |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.start, |
|
children: [ |
|
const SizedBox(width: 10.0), |
|
TextButton(child: Text( |
|
this._myAccount ? lang.walletInputAccount: lang.walletBetweenAccount, |
|
), onPressed: () => setState(() { |
|
this._myAccount = !this._myAccount; |
|
if (this._selectAddress.length == 0) { |
|
this._selectAddress = widget.addresses[0].address; |
|
} |
|
}) |
|
), |
|
] |
|
), |
|
widget.token.isNft() |
|
? Container( |
|
padding: const EdgeInsets.only(top: 15.0, bottom: 20.0), |
|
child: Row( |
|
children: [ |
|
Expanded( |
|
child: this._nftInput |
|
? InputText( |
|
icon: Icons.verified, |
|
text: 'TokenID', |
|
controller: _amountController, |
|
focus: _amountFocus) |
|
: Padding( |
|
padding: const EdgeInsets.only(left: 20.0), |
|
child: DropdownButtonHideUnderline( |
|
child: Theme( |
|
data: Theme.of(context).copyWith( |
|
canvasColor: color.background, |
|
), |
|
child: DropdownButton<String>( |
|
iconEnabledColor: Color(0xFFADB0BB), |
|
isExpanded: true, |
|
value: this._selectNft, |
|
onChanged: (String? value) { |
|
if (value != null) { |
|
setState(() { |
|
this._selectNft = value; |
|
}); |
|
} |
|
}, |
|
items: this._nft.map((value) { |
|
return DropdownMenuItem<String>( |
|
value: value, |
|
child: Text(pidPrint(value, 6)), |
|
); |
|
}).toList(), |
|
), |
|
), |
|
) |
|
) |
|
), |
|
SizedBox(width: 80.0, |
|
child: IconButton(icon: this._nftInput ? Icon(Icons.fact_check, color: color.primary) : Icon(Icons.edit, color: Colors.green), |
|
onPressed: () => setState(() { |
|
this._nftInput = !this._nftInput; |
|
}) |
|
), |
|
) |
|
] |
|
) |
|
) |
|
: Container( |
|
padding: const EdgeInsets.only(top: 15.0, bottom: 20.0), |
|
child: Row( |
|
children: [ |
|
Expanded( |
|
child: InputText( |
|
icon: Icons.paid, |
|
text: '0.0', |
|
controller: _amountController, |
|
focus: _amountFocus), |
|
), |
|
SizedBox(width: 80.0, |
|
child: IconButton(icon: Text('Max', style: TextStyle(color: color.primary)), |
|
onPressed: () => setState(() { |
|
if (widget.token.chain.isMain()) { |
|
final a = widget.token.amount - double.parse(this._gas); |
|
_amountController.text = "${a}"; |
|
} else { |
|
_amountController.text = "${widget.token.amount}"; |
|
} |
|
}) |
|
), |
|
) |
|
] |
|
) |
|
), |
|
this._checked |
|
? ButtonText( |
|
text: lang.send, |
|
action: () { |
|
String to = _nameController.text.trim(); |
|
if (_myAccount) { |
|
to = this._selectAddress; |
|
} |
|
String a = _amountController.text.trim(); |
|
if (widget.token.isNft() && !_nftInput) { |
|
a = this._selectNft; |
|
} |
|
if (a.length == 0 || (!widget.token.isNft() && double.parse(a) == 0)) { |
|
_amountFocus.requestFocus(); |
|
return; |
|
} |
|
if (to.length < 20) { |
|
_nameFocus.requestFocus(); |
|
return; |
|
} |
|
final amount = restoreBalance(a, widget.token.decimal); |
|
final pid = context.read<AccountProvider>().id; |
|
showShadowDialog( |
|
context, |
|
Icons.security_rounded, |
|
lang.verifyPin, |
|
PinWords( |
|
pid: pid, |
|
callback: (key) async { |
|
Navigator.of(context).pop(); |
|
rpc.send('wallet-transfer', [ |
|
widget.token.chain.toInt(), widget.network.toInt(), |
|
widget.address.id, to, restoreBalance(amount, widget.token.decimal), |
|
widget.token.contract, key, |
|
]); |
|
Navigator.of(context).pop(); |
|
}), |
|
0.0, |
|
); |
|
}) |
|
: ButtonText( |
|
enable: !this._checking, |
|
text: this._checking ? lang.waiting : lang.check, |
|
action: () { |
|
String to = _nameController.text.trim(); |
|
if (_myAccount) { |
|
to = this._selectAddress; |
|
} |
|
String a = _amountController.text.trim(); |
|
if (widget.token.isNft() && !_nftInput) { |
|
a = this._selectNft; |
|
} |
|
if (a.length == 0 || (!widget.token.isNft() && double.parse(a) == 0)) { |
|
_amountFocus.requestFocus(); |
|
return; |
|
} |
|
if (to.length < 20) { |
|
_nameFocus.requestFocus(); |
|
return; |
|
} |
|
final amount = restoreBalance(a, widget.token.decimal); |
|
_gasPrice(to, amount); |
|
setState(() { |
|
this._checking = true; |
|
}); |
|
}) |
|
] |
|
); |
|
} |
|
} |
|
|
|
class _ImportNft extends StatefulWidget { |
|
final Address address; |
|
final Token token; |
|
_ImportNft({Key? key, required this.address, required this.token}) : super(key: key); |
|
|
|
@override |
|
_ImportNftState createState() => _ImportNftState(); |
|
} |
|
|
|
class _ImportNftState extends State<_ImportNft> { |
|
TextEditingController _nameController = TextEditingController(); |
|
FocusNode _nameFocus = FocusNode(); |
|
List<String> _nft = []; |
|
bool _searching = false; |
|
String _error = ''; |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
_nameController.addListener(() { |
|
setState(() { |
|
this._error = ''; |
|
}); |
|
}); |
|
_getNft(); |
|
} |
|
|
|
_getNft() async { |
|
final res = await httpPost('wallet-nft', [widget.address.id, widget.token.id]); |
|
if (res.isOk) { |
|
final a = res.params[0]; |
|
final t = res.params[1]; |
|
if (a == widget.address.id && t == widget.token.id) { |
|
this._nft.clear(); |
|
res.params[2].forEach((hash) { |
|
this._nft.add(hash); |
|
}); |
|
} |
|
setState(() {}); |
|
} |
|
} |
|
|
|
_search(String hash) async { |
|
final res = await httpPost('wallet-nft-add', [widget.address.id, widget.token.id, hash]); |
|
if (res.isOk) { |
|
final a = res.params[0]; |
|
final t = res.params[1]; |
|
if (a == widget.address.id && t == widget.token.id) { |
|
if (!this._nft.contains(res.params[2])) { |
|
this._nft.add(res.params[2]); |
|
} |
|
} |
|
this._searching = false; |
|
this._error = ''; |
|
} else { |
|
this._searching = false; |
|
this._error = res.error; |
|
} |
|
setState(() {}); |
|
} |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
final color = Theme.of(context).colorScheme; |
|
final lang = AppLocalizations.of(context); |
|
final height = MediaQuery.of(context).size.height; |
|
|
|
return Column( |
|
children: [ |
|
InputText( |
|
icon: Icons.verified, |
|
text: 'TokenID', |
|
controller: _nameController, |
|
focus: _nameFocus, |
|
enabled: !this._searching |
|
), |
|
Padding( |
|
padding: EdgeInsets.symmetric(vertical: this._error.length > 1 ? 8.0 : 0), |
|
child: Text(this._error, style: TextStyle(color: Colors.red)), |
|
), |
|
ButtonText( |
|
enable: !this._searching, |
|
text: this._searching ? lang.waiting : lang.add, |
|
action: () { |
|
final hash = _nameController.text.trim(); |
|
if (hash.length < 1) { |
|
return; |
|
} |
|
_search(hash); |
|
setState(() { |
|
this._searching = true; |
|
}); |
|
}), |
|
Row( |
|
mainAxisAlignment: MainAxisAlignment.start, |
|
children: [ |
|
Padding( |
|
padding: const EdgeInsets.only(top: 20.0, bottom: 10.0), |
|
child: Text('NFT:', textAlign: TextAlign.left), |
|
)]), |
|
Container( |
|
height: height-300 < 200 ? height-300: 200, |
|
child: ListView.separated( |
|
separatorBuilder: (BuildContext context, int index) => const Divider(), |
|
itemCount: this._nft.length, |
|
itemBuilder: (BuildContext context, int index) { |
|
final hash = this._nft[index]; |
|
return ListTile( |
|
title: Text('TokenID: ' + pidPrint(hash, 6)), |
|
trailing: IconButton(icon: Icon(Icons.link, color: color.primary), |
|
onPressed: () { |
|
launch(widget.token.nftUrl(hash)); |
|
} |
|
), |
|
); |
|
} |
|
), |
|
) |
|
] |
|
); |
|
} |
|
}
|
|
|