diff --git a/lib/apps/wallet/models.dart b/lib/apps/wallet/models.dart index 78163f4..dd06805 100644 --- a/lib/apps/wallet/models.dart +++ b/lib/apps/wallet/models.dart @@ -79,7 +79,7 @@ extension NetworkExtension on Network { case Network.EthTestRinkeby: return ['Rinkeby Test Network', Colors.orange]; case Network.EthTestKovan: - return ['Rinkeby Test Network', Colors.orange]; + return ['Kovan Test Network', Colors.orange]; case Network.EthLocal: return ['Localhost 8545', Color(0xFF6174FF)]; case Network.BtcMain: @@ -188,6 +188,25 @@ class Address { } } + Token mainToken(Network network) { + switch (this.chain) { + case ChainToken.ETH: + case ChainToken.ERC20: + case ChainToken.ERC721: + Token token = Token.eth(network); + if (this.balances.containsKey(network)) { + token.balance(this.balances[network]!); + } + return token; + case ChainToken.BTC: + Token token = Token.btc(network); + if (this.balances.containsKey(network)) { + token.balance(this.balances[network]!); + } + return token; + } + } + split_balance(String s) { if (s.length > 0) { Map balances = {}; @@ -240,13 +259,14 @@ class Token { } } - Token.fromList(List params) { + Token.fromList(List params, String balance) { this.id = params[0]; this.chain = ChainTokenExtension.fromInt(params[1]); this.network = NetworkExtension.fromInt(params[2]); this.name = params[3]; this.contract = params[4]; this.decimal = params[5]; + this.balance(balance); } Token.eth(Network network) { @@ -259,6 +279,15 @@ class Token { this.decimal = 8; } + String short() { + final len = this.contract.length; + if (len > 10) { + return this.contract.substring(0, 6) + '...' + this.contract.substring(len - 4, len); + } else { + return this.contract; + } + } + balance(String number) { this.balanceString = number; this.amount = double.parse(unit_balance(number, this.decimal, 8)); diff --git a/lib/apps/wallet/page.dart b/lib/apps/wallet/page.dart index ecbbd06..4a3c425 100644 --- a/lib/apps/wallet/page.dart +++ b/lib/apps/wallet/page.dart @@ -7,9 +7,11 @@ import 'package:esse/utils/better_print.dart'; import 'package:esse/l10n/localizations.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/global.dart'; +import 'package:esse/provider.dart'; import 'package:esse/options.dart'; +import 'package:esse/global.dart'; import 'package:esse/rpc.dart'; import 'package:esse/apps/wallet/models.dart'; @@ -34,18 +36,14 @@ class _WalletDetailState extends State with SingleTickerProviderSt Token _mainToken = Token(); List _tokens = []; - List tokens = [ - ['ETH', '2000', '2000', 'assets/logo/logo_eth.png'], - ['USDT', '2000', '2000', 'assets/logo/logo_tether.png'], - ['XXX', '100', '1000', 'assets/logo/logo_erc20.png'], - ['wBTC', '100', '1000', 'assets/logo/logo_btc.png'], - ]; - @override void initState() { _tabController = new TabController(length: 2, vsync: this); + rpc.addListener('wallet-generate', _walletGenerate, false); + rpc.addListener('wallet-import', _walletGenerate, false); rpc.addListener('wallet-balance', _walletBalance, false); + super.initState(); Future.delayed(Duration.zero, _load); } @@ -69,13 +67,27 @@ class _WalletDetailState extends State with SingleTickerProviderSt final address = params[0]; final network = NetworkExtension.fromInt(params[1]); if (address == this._selectedAddress!.address && network == this._selectedNetwork!) { - final contract = params[2]; - final balance = params[3]; - - // TODO check token. + final balance = params[2]; - this._mainToken = Token.eth(network); - this._mainToken.balance(balance); + 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].balance(balance); + } + } else { + this._mainToken = Token.eth(network); + this._mainToken.balance(balance); + } setState(() {}); } } @@ -108,6 +120,10 @@ class _WalletDetailState extends State with SingleTickerProviderSt this._selectedNetwork!.toInt(), this._selectedAddress!.address ]); } + this._mainToken = address.mainToken(this._selectedNetwork!); + for (var i = 0; i < this._tokens.length; i++) { + this._tokens[i].balance('0'); + } } _changeNetwork(Network network) { @@ -115,6 +131,7 @@ class _WalletDetailState extends State with SingleTickerProviderSt rpc.send('wallet-balance', [ this._selectedNetwork!.toInt(), this._selectedAddress!.address ]); + this._tokens.clear(); } @override @@ -136,7 +153,8 @@ class _WalletDetailState extends State with SingleTickerProviderSt child: ElevatedButton( style: ElevatedButton.styleFrom(onPrimary: color.surface), onPressed: () { - rpc.send('wallet-generate', [ChainToken.ETH.toInt(), ""]); + final pin = context.read().activedAccount.pin; + rpc.send('wallet-generate', [ChainToken.ETH.toInt(), pin]); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), @@ -213,7 +231,9 @@ class _WalletDetailState extends State with SingleTickerProviderSt 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 { @@ -360,32 +380,37 @@ class _WalletDetailState extends State with SingleTickerProviderSt children: [ ListView.separated( separatorBuilder: (BuildContext context, int index) => const Divider(), - itemCount: tokens.length + 1, + itemCount: this._tokens.length + 1, itemBuilder: (BuildContext context, int index) { - if (index == tokens.length) { + if (index == this._tokens.length) { return TextButton( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: Text('Add new Token' + ' ( ERC20 / ERC721 )') ), - onPressed: () { - // - }, + 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(tokens[index][3]), + image: AssetImage(token.logo), fit: BoxFit.cover, ), ), ), - title: Text(tokens[index][1] + ' ' + tokens[index][0]), - subtitle: Text('\$' + tokens[index][2]), + title: Text("${token.amount} ${token.name}",), + subtitle: Text(token.short()), trailing: IconButton(icon: Icon(Icons.arrow_forward_ios), onPressed: () {}), ); @@ -422,3 +447,120 @@ class _WalletDetailState extends State with SingleTickerProviderSt ); } } + +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; + } + rpc.send('wallet-import', [chain.toInt(), secret]); + 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); + }), + ] + ); + } +} diff --git a/lib/l10n/localizations.dart b/lib/l10n/localizations.dart index ec5fb61..bbcce12 100644 --- a/lib/l10n/localizations.dart +++ b/lib/l10n/localizations.dart @@ -263,6 +263,8 @@ abstract class AppLocalizations { String get wallet; String get walletIntro; + String get secretKey; + String get contract; } class _AppLocalizationsDelegate diff --git a/lib/l10n/localizations_en.dart b/lib/l10n/localizations_en.dart index 02b5314..0dc4880 100644 --- a/lib/l10n/localizations_en.dart +++ b/lib/l10n/localizations_en.dart @@ -429,4 +429,8 @@ class AppLocalizationsEn extends AppLocalizations { String get wallet => 'Wallet'; @override String get walletIntro => 'Manage your own cryptocurrency.'; + @override + String get secretKey => 'Secret Key'; + @override + String get contract => 'Contract Address'; } diff --git a/lib/l10n/localizations_zh.dart b/lib/l10n/localizations_zh.dart index 620a944..7ca9970 100644 --- a/lib/l10n/localizations_zh.dart +++ b/lib/l10n/localizations_zh.dart @@ -429,4 +429,8 @@ class AppLocalizationsZh extends AppLocalizations { String get wallet => '钱包'; @override String get walletIntro => '管理自己的加密货币。'; + @override + String get secretKey => '私钥'; + @override + String get contract => '合约地址'; } diff --git a/src/account.rs b/src/account.rs index dff07c1..1dfac01 100644 --- a/src/account.rs +++ b/src/account.rs @@ -8,7 +8,7 @@ use tdn_storage::local::{DStorage, DsValue}; use crate::utils::crypto::{check_pin, decrypt, decrypt_multiple, encrypt_multiple, hash_pin}; -fn mnemonic_lang_to_i64(lang: Language) -> i64 { +fn _mnemonic_lang_to_i64(lang: Language) -> i64 { match lang { Language::English => 0, Language::SimplifiedChinese => 1, diff --git a/src/apps/wallet/mod.rs b/src/apps/wallet/mod.rs index 3a9db84..31e26a8 100644 --- a/src/apps/wallet/mod.rs +++ b/src/apps/wallet/mod.rs @@ -2,3 +2,655 @@ mod models; mod rpc; pub(crate) use rpc::new_rpc_handler; + +pub(crate) const ERC20_ABI: &'static str = r#" +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "payable": true, + "stateMutability": "payable", + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + } +] +"#; + +pub(crate) const ERC721_ABI: &'static str = r#" +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_tokenId", + "type": "uint256" + } + ], + "name": "mintTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "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": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] +"#; diff --git a/src/apps/wallet/models.rs b/src/apps/wallet/models.rs index 6b08362..2fcd1d3 100644 --- a/src/apps/wallet/models.rs +++ b/src/apps/wallet/models.rs @@ -50,6 +50,7 @@ impl ChainToken { } } +#[derive(Clone, Copy)] pub(crate) enum Network { EthMain, EthTestRopsten, @@ -219,6 +220,15 @@ impl Address { } pub fn insert(&mut self, db: &DStorage) -> Result<()> { + let matrix = db.query(&format!( + "SELECT id FROM addresses WHERE chain = {} AND address = '{}'", + self.chain.to_i64(), + self.address + ))?; + if matrix.len() > 0 { + return Ok(()); + } + let sql = format!( "INSERT INTO addresses (chain, indx, name, address, secret, balance) VALUES ({}, {}, '{}', '{}', '{}', '{}')", self.chain.to_i64(), @@ -340,6 +350,16 @@ impl Token { } pub fn insert(&mut self, db: &DStorage) -> Result<()> { + let matrix = db.query(&format!( + "SELECT id FROM tokens WHERE network = {} AND contract = '{}'", + self.network.to_i64(), + self.contract + ))?; + if matrix.len() > 0 { + return Ok(()); + } + + // check exists let sql = format!( "INSERT INTO tokens (chain, network, name, contract, decimal) VALUES ({}, {}, '{}', '{}', {})", self.chain.to_i64(), @@ -365,6 +385,18 @@ impl Token { Ok(tokens) } + pub fn get(db: &DStorage, id: &i64) -> Result { + let mut matrix = db.query(&format!( + "SELECT id, chain, network, name, contract, decimal FROM tokens where id = {}", + id + ))?; + if matrix.len() > 0 { + let values = matrix.pop().unwrap(); // safe unwrap() + return Ok(Self::from_values(values)); + } + Err(anyhow!("token is missing!")) + } + pub fn _delete(db: &DStorage, id: &i64) -> Result<()> { let sql = format!("DELETE FROM tokens WHERE id = {}", id); db.delete(&sql)?; diff --git a/src/apps/wallet/rpc.rs b/src/apps/wallet/rpc.rs index d64a2bf..6d91152 100644 --- a/src/apps/wallet/rpc.rs +++ b/src/apps/wallet/rpc.rs @@ -8,11 +8,14 @@ use tdn::types::{ use tdn_did::{generate_btc_account, generate_eth_account, secp256k1::SecretKey}; use tdn_storage::local::DStorage; use tokio::sync::mpsc::Sender; -use web3::signing::Key; +use web3::{contract::Contract, signing::Key, types::Address as EthAddress, Web3}; use crate::{rpc::RpcState, storage::wallet_db, utils::crypto::encrypt}; -use super::models::{Address, ChainToken, Network, Token}; +use super::{ + models::{Address, ChainToken, Network, Token}, + ERC20_ABI, ERC721_ABI, +}; const WALLET_DEFAULT_PIN: &'static str = "walletissafe"; @@ -30,15 +33,24 @@ fn res_balance( gid: GroupId, address: &str, network: &Network, - contract: &str, balance: &str, + token: Option<&Token>, ) -> RpcParam { - rpc_response( - 0, - "wallet-balance", - json!([address, network.to_i64(), contract, balance]), - gid, - ) + if let Some(t) = token { + rpc_response( + 0, + "wallet-balance", + json!([address, network.to_i64(), balance, t.to_rpc()]), + gid, + ) + } else { + rpc_response( + 0, + "wallet-balance", + json!([address, network.to_i64(), balance]), + gid, + ) + } } async fn loop_token( @@ -47,49 +59,117 @@ async fn loop_token( gid: GroupId, network: Network, address: String, + c_token: Option, ) -> Result<()> { // loop get balance of all tokens. let node = network.node(); let chain = network.chain(); let tokens = Token::list(&db, &network)?; - match chain { - ChainToken::ETH => { - let transport = web3::transports::Http::new(node).unwrap(); - let web3 = web3::Web3::new(transport); - let balance = web3 - .eth() - .balance(address.parse().unwrap(), None) - .await - .unwrap(); - let balance = balance.to_string(); - let _ = Address::update_balance(&db, &address, &network, &balance); - let res = res_balance(gid, &address, &network, "", &balance); - sender.send(SendMessage::Rpc(0, res, true)).await?; - - for token in tokens { - match token.chain { - ChainToken::ERC20 => { - // - } - ChainToken::ERC721 => { - // - } - _ => { - // - } + if let Some(token) = c_token { + let balance = token_balance(&token.contract, &address, &node, &token.chain).await?; + let res = res_balance(gid, &address, &network, &balance, Some(&token)); + sender.send(SendMessage::Rpc(0, res, true)).await?; + } else { + match chain { + ChainToken::ETH => { + let transport = web3::transports::Http::new(node)?; + let web3 = Web3::new(transport); + let balance = web3.eth().balance(address.parse()?, None).await?; + let balance = balance.to_string(); + let _ = Address::update_balance(&db, &address, &network, &balance); + let res = res_balance(gid, &address, &network, &balance, None); + sender.send(SendMessage::Rpc(0, res, true)).await?; + + for token in tokens { + let balance = + token_balance(&token.contract, &address, &node, &token.chain).await?; + let res = res_balance(gid, &address, &network, &balance, Some(&token)); + sender.send(SendMessage::Rpc(0, res, true)).await?; } } + ChainToken::BTC => { + // TODO + } + _ => panic!("nerver here!"), } - ChainToken::BTC => { - // TODO - } - _ => panic!("nerver here!"), } Ok(()) } +async fn token_check( + sender: Sender, + db: DStorage, + gid: GroupId, + chain: ChainToken, + network: Network, + address: String, + c_str: String, +) -> Result<()> { + let account: EthAddress = address.parse()?; + let addr: EthAddress = c_str.parse()?; + + let abi = match chain { + ChainToken::ERC20 => ERC20_ABI, + ChainToken::ERC721 => ERC721_ABI, + _ => return Err(anyhow!("not supported")), + }; + let node = network.node(); + let transport = web3::transports::Http::new(node)?; + let web3 = Web3::new(transport); + let contract = Contract::from_json(web3.eth(), addr, abi.as_bytes())?; + + let symbol: String = contract + .query("symbol", (), None, Default::default(), None) + .await?; + + let decimal: u64 = match chain { + ChainToken::ERC20 => { + contract + .query("decimals", (), None, Default::default(), None) + .await? + } + _ => 0, + }; + + let mut token = Token::new(chain, network, symbol, c_str, decimal as i64); + token.insert(&db)?; + + let balance: web3::types::U256 = contract + .query("balanceOf", (account,), None, Default::default(), None) + .await?; + let balance = balance.to_string(); + let res = res_balance(gid, &address, &network, &balance, Some(&token)); + sender.send(SendMessage::Rpc(0, res, true)).await?; + + Ok(()) +} + +async fn token_balance( + c_str: &str, + address: &str, + node: &str, + chain: &ChainToken, +) -> Result { + let addr: EthAddress = c_str.parse()?; + let account: EthAddress = address.parse()?; + let abi = match chain { + ChainToken::ERC20 => ERC20_ABI, + ChainToken::ERC721 => ERC721_ABI, + _ => return Err(anyhow!("not supported")), + }; + + let transport = web3::transports::Http::new(node)?; + let web3 = Web3::new(transport); + let contract = Contract::from_json(web3.eth(), addr, abi.as_bytes())?; + + let balance: web3::types::U256 = contract + .query("balanceOf", (account,), None, Default::default(), None) + .await?; + Ok(balance.to_string()) +} + pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { handler.add_method("wallet-echo", |_, params, _| async move { Ok(HandleResult::rpc(json!(params))) @@ -168,14 +248,40 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler) { |gid: GroupId, params: Vec, state: Arc| async move { let network = Network::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?); let address = params[1].as_str().ok_or(RpcError::ParseError)?.to_owned(); - println!("start wallet balances"); let group_lock = state.group.read().await; let db = wallet_db(group_lock.base(), &gid)?; let sender = group_lock.sender(); drop(group_lock); - tokio::spawn(loop_token(sender, db, gid, network, address)); + let c_str = if params.len() == 4 { + let cid = params[2].as_i64().ok_or(RpcError::ParseError)?; + let token = Token::get(&db, &cid)?; + Some(token) + } else { + None + }; + + tokio::spawn(loop_token(sender, db, gid, network, address, c_str)); + + Ok(HandleResult::new()) + }, + ); + + handler.add_method( + "wallet-token-import", + |gid: GroupId, params: Vec, state: Arc| async move { + let chain = ChainToken::from_i64(params[0].as_i64().ok_or(RpcError::ParseError)?); + let network = Network::from_i64(params[1].as_i64().ok_or(RpcError::ParseError)?); + let address = params[2].as_str().ok_or(RpcError::ParseError)?.to_owned(); + let c_str = params[3].as_str().ok_or(RpcError::ParseError)?.to_owned(); + + let group_lock = state.group.read().await; + let db = wallet_db(group_lock.base(), &gid)?; + let sender = group_lock.sender(); + drop(group_lock); + + tokio::spawn(token_check(sender, db, gid, chain, network, address, c_str)); Ok(HandleResult::new()) }, diff --git a/src/migrate/wallet.rs b/src/migrate/wallet.rs index 7dabc03..278861b 100644 --- a/src/migrate/wallet.rs +++ b/src/migrate/wallet.rs @@ -1,5 +1,5 @@ #[rustfmt::skip] -pub(super) const WALLET_VERSIONS: [&str; 2] = [ +pub(super) const WALLET_VERSIONS: [&str; 3] = [ "CREATE TABLE IF NOT EXISTS addresses( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, chain INTEGER NOT NULL, @@ -15,4 +15,5 @@ pub(super) const WALLET_VERSIONS: [&str; 2] = [ name TEXT NOT NULL, contract TEXT NOT NULL, decimal INTEGER NOT NULL);", + "INSERT INTO tokens (chain, network, name, contract, decimal) VALUES (2, 1, 'USDT', '0xdac17f958d2ee523a2206206994597c13d831ec7', 6);", // default eth mainnet USDT. ];