Browse Source

Wallet: Add transfer function

pull/18/head
Sun 4 years ago
parent
commit
cd3eb85f3d
  1. 93
      lib/apps/wallet/models.dart
  2. 413
      lib/apps/wallet/page.dart
  3. 2
      lib/l10n/localizations.dart
  4. 4
      lib/l10n/localizations_en.dart
  5. 4
      lib/l10n/localizations_zh.dart
  6. 17
      src/apps/wallet/models.rs
  7. 221
      src/apps/wallet/rpc.rs

93
lib/apps/wallet/models.dart

@ -19,6 +19,18 @@ extension ChainTokenExtension on ChainToken { @@ -19,6 +19,18 @@ extension ChainTokenExtension on ChainToken {
}
}
bool isMain() {
switch (this) {
case ChainToken.ETH:
return true;
case ChainToken.ERC20:
case ChainToken.ERC721:
return false;
case ChainToken.BTC:
return true;
}
}
String get symbol {
switch (this) {
case ChainToken.ETH:
@ -89,6 +101,25 @@ extension NetworkExtension on Network { @@ -89,6 +101,25 @@ extension NetworkExtension on Network {
}
}
String txUrl() {
switch (this) {
case Network.EthMain:
return 'https://etherscan.io/tx/';
case Network.EthTestRopsten:
return 'https://ropsten.etherscan.io/tx/';
case Network.EthTestRinkeby:
return 'https://rinkeby.etherscan.io/tx/';
case Network.EthTestKovan:
return 'https://kovan.etherscan.io/tx/';
case Network.EthLocal:
return 'https://etherscan.io/tx/';
case Network.BtcMain:
return 'https://www.blockchain.com/btc/tx/';
case Network.BtcLocal:
return 'https://www.blockchain.com/btc/tx/';
}
}
int toInt() {
switch (this) {
case Network.EthMain:
@ -179,9 +210,9 @@ class Address { @@ -179,9 +210,9 @@ class Address {
case ChainToken.ETH:
case ChainToken.ERC20:
case ChainToken.ERC721:
return unit_balance(s, 18, 4);
return unitBalance(s, 18, 4);
case ChainToken.BTC:
return unit_balance(s, 8, 4);
return unitBalance(s, 8, 4);
}
} else {
return '0.0';
@ -290,19 +321,73 @@ class Token { @@ -290,19 +321,73 @@ class Token {
balance(String number) {
this.balanceString = number;
this.amount = double.parse(unit_balance(number, this.decimal, 8));
this.amount = double.parse(unitBalance(number, this.decimal, 8));
}
}
class Transaction {
String hash = '';
String to = '';
String short_hash() {
final len = this.hash.length;
if (len > 12) {
return this.hash.substring(0, 8) + '...' + this.hash.substring(len - 4, len);
} else {
return this.hash;
}
}
String short_to() {
final len = this.to.length;
if (len > 10) {
return this.to.substring(0, 6) + '...' + this.to.substring(len - 4, len);
} else {
return this.to;
}
}
Transaction.fromList(List params) {
this.hash = params[0];
this.to = params[1];
}
}
String unit_balance(String number, int decimal, int limit) {
String unitBalance(String number, int decimal, int limit) {
final pad = number.length - (decimal + 1); // 0.00..00
if (pad < 0) {
number = ('0' * (-pad)) + number;
}
String right = number.substring(number.length - decimal, number.length);
final left = number.substring(0, number.length - decimal);
if (limit == 0) {
return left;
}
if (right.length > limit) {
right = right.substring(0, limit);
}
return left + '.' + right;
}
String restoreBalance(String number, int decimal) {
if (decimal == 0) {
return number;
}
final s = number.split('.');
int pad = decimal;
String right = '';
if (s.length > 1 && s[1].length > 0) {
if (s[1].length > decimal) {
right = s[1].substring(0, decimal);
} else {
right = s[1] + ('0' * (decimal - s[1].length));
}
} else {
right = '0' * decimal;
}
return s[0] + right;
}

413
lib/apps/wallet/page.dart

@ -1,10 +1,13 @@ @@ -1,10 +1,13 @@
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';
@ -35,6 +38,7 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -35,6 +38,7 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
Token _mainToken = Token();
List<Token> _tokens = [];
List<Transaction> _txs = [];
@override
void initState() {
@ -44,6 +48,7 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -44,6 +48,7 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
rpc.addListener('wallet-import', _walletGenerate, false);
rpc.addListener('wallet-token', _walletToken, false);
rpc.addListener('wallet-balance', _walletBalance, false);
rpc.addListener('wallet-transfer', _walletTransfer, false);
super.initState();
Future.delayed(Duration.zero, _load);
@ -104,6 +109,23 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -104,6 +109,23 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
}
}
_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(Global.httpRpc, 'wallet-list', []);
if (res.isOk) {
@ -337,27 +359,63 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -337,27 +359,63 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
"${this._mainToken.amount} ${this._mainToken.name}",
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)),
),
Text('\$1000', style: TextStyle(color: Color(0xFFADB0BB))),
//Text('\$0.0', style: TextStyle(color: Color(0xFFADB0BB))),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
setState(() {});
},
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: Text('Send', style: TextStyle(color: Colors.white)))
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: () {
setState(() {});
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),
@ -365,7 +423,15 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -365,7 +423,15 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
color: Color(0xFF6174FF),
borderRadius: BorderRadius.circular(25.0)
),
child: Center(child: Text('Receive', style: TextStyle(color: Colors.white)))
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))
]
)
)
)
),
]
@ -419,19 +485,47 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -419,19 +485,47 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
),
title: Text("${token.amount} ${token.name}",),
subtitle: Text(token.short()),
trailing: IconButton(icon: Icon(Icons.arrow_forward_ios),
onPressed: () {}),
trailing: 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: 10,
itemCount: this._txs.length + 1,
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text('TODO ${index}'),
);
if (index == this._txs.length) {
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);
}
),
);
}
}
),
],
@ -572,3 +666,296 @@ class _ImportTokenState extends State<_ImportToken> { @@ -572,3 +666,296 @@ class _ImportTokenState extends State<_ImportToken> {
);
}
}
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;
@override
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {
this._checked = false;
});
});
_amountController.addListener(() {
setState(() {
this._checked = false;
});
});
}
_gasPrice(String to, String amount) async {
final res = await httpPost(Global.httpRpc, '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;
}
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.amount} ${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),
(this._networkError.length > 1)
? Text(this._networkError, style: TextStyle(color: Colors.red))
: RichText(
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.address + ' (0x...)',
controller: _nameController,
focus: _nameFocus
)),
SizedBox(width: 80.0,
child: IconButton(icon: Icon(Icons.qr_code, color: color.primary),
onPressed: () {
//
})
)
])
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(width: 10.0),
TextButton(child: Text(
this._myAccount ? 'Input Account' : 'Between Accounts'
), onPressed: () => setState(() {
this._myAccount = !this._myAccount;
if (this._selectAddress.length == 0) {
this._selectAddress = widget.addresses[0].address;
}
})
),
]
),
Container(
padding: const EdgeInsets.only(top: 15.0, bottom: 20.0),
child: Row(
children: [
Expanded(
child: InputText(
icon: Icons.paid,
text: 'Amount(0) or NFT(0x...)',
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;
}
final a = _amountController.text.trim();
if (double.parse(a) == 0) {
_amountFocus.requestFocus();
return;
}
if (to.length < 20) {
_nameFocus.requestFocus();
return;
}
final amount = restoreBalance(a, widget.token.decimal);
final gid = context.read<AccountProvider>().activedAccount.gid;
showShadowDialog(
context,
Icons.security_rounded,
lang.verifyPin,
PinWords(
gid: gid,
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(
text: lang.check,
action: () {
String to = _nameController.text.trim();
if (_myAccount) {
to = this._selectAddress;
}
final a = _amountController.text.trim();
if (a.length == 0 || double.parse(a) == 0) {
_amountFocus.requestFocus();
return;
}
if (to.length < 20) {
_nameFocus.requestFocus();
return;
}
final amount = restoreBalance(a, widget.token.decimal);
_gasPrice(to, amount);
})
]
);
}
}

2
lib/l10n/localizations.dart

@ -61,6 +61,7 @@ abstract class AppLocalizations { @@ -61,6 +61,7 @@ abstract class AppLocalizations {
String get address;
String get remark;
String get send;
String get receive;
String get sended;
String get resend;
String get ignore;
@ -104,6 +105,7 @@ abstract class AppLocalizations { @@ -104,6 +105,7 @@ abstract class AppLocalizations {
String get link;
String get rename;
String get moveTo;
String get check;
// theme
String get themeDark;

4
lib/l10n/localizations_en.dart

@ -47,6 +47,8 @@ class AppLocalizationsEn extends AppLocalizations { @@ -47,6 +47,8 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get send => 'Send';
@override
String get receive => 'Receive';
@override
String get sended => 'Sended';
@override
String get resend => 'Resend';
@ -132,6 +134,8 @@ class AppLocalizationsEn extends AppLocalizations { @@ -132,6 +134,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get rename => 'Rename';
@override
String get moveTo => 'Move to';
@override
String get check => 'Check';
// theme
@override

4
lib/l10n/localizations_zh.dart

@ -47,6 +47,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -47,6 +47,8 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get send => '发送';
@override
String get receive => '接收';
@override
String get sended => '已发送';
@override
String get resend => '重新发送';
@ -132,6 +134,8 @@ class AppLocalizationsZh extends AppLocalizations { @@ -132,6 +134,8 @@ class AppLocalizationsZh extends AppLocalizations {
String get rename => '重命名';
@override
String get moveTo => '移动到';
@override
String get check => '检查';
// theme
@override

17
src/apps/wallet/models.rs

@ -244,9 +244,8 @@ impl Address { @@ -244,9 +244,8 @@ impl Address {
}
pub fn list(db: &DStorage) -> Result<Vec<Self>> {
let matrix = db.query(&format!(
"SELECT id, chain, indx, name, address, secret, balance FROM addresses"
))?;
let matrix =
db.query("SELECT id, chain, indx, name, address, secret, balance FROM addresses")?;
let mut addresses = vec![];
for values in matrix {
addresses.push(Self::from_values(values));
@ -254,6 +253,18 @@ impl Address { @@ -254,6 +253,18 @@ impl Address {
Ok(addresses)
}
pub fn get(db: &DStorage, id: &i64) -> Result<Self> {
let mut matrix = db.query(&format!(
"SELECT id, chain, indx, name, address, secret, balance FROM addresses WHERE id = {}",
id
))?;
if matrix.len() > 0 {
let values = matrix.pop().unwrap(); // safe unwrap()
return Ok(Self::from_values(values));
}
Err(anyhow!("address is missing!"))
}
pub fn next_index(db: &DStorage, chain: &ChainToken) -> Result<u32> {
let mut matrix = db.query(&format!(
"SELECT indx FROM addresses where chain = {} AND secret = '' ORDER BY indx ASC",

221
src/apps/wallet/rpc.rs

@ -8,9 +8,18 @@ use tdn::types::{ @@ -8,9 +8,18 @@ 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::{contract::Contract, signing::Key, types::Address as EthAddress, Web3};
use web3::{
contract::{tokens::Tokenize, Contract},
signing::Key,
types::{Address as EthAddress, Bytes, CallRequest, TransactionParameters, U256},
Web3,
};
use crate::{rpc::RpcState, storage::wallet_db, utils::crypto::encrypt};
use crate::{
rpc::RpcState,
storage::wallet_db,
utils::crypto::{decrypt, encrypt},
};
use super::{
models::{Address, ChainToken, Network, Token},
@ -145,7 +154,7 @@ async fn token_check( @@ -145,7 +154,7 @@ async fn token_check(
let mut token = Token::new(chain, network, symbol, c_str, decimal as i64);
token.insert(&db)?;
let balance: web3::types::U256 = contract
let balance: U256 = contract
.query("balanceOf", (account,), None, Default::default(), None)
.await?;
let balance = balance.to_string();
@ -163,6 +172,7 @@ async fn token_balance( @@ -163,6 +172,7 @@ async fn token_balance(
) -> Result<String> {
let addr: EthAddress = c_str.parse()?;
let account: EthAddress = address.parse()?;
let abi = match chain {
ChainToken::ERC20 => ERC20_ABI,
ChainToken::ERC721 => ERC721_ABI,
@ -173,12 +183,125 @@ async fn token_balance( @@ -173,12 +183,125 @@ async fn token_balance(
let web3 = Web3::new(transport);
let contract = Contract::from_json(web3.eth(), addr, abi.as_bytes())?;
let balance: web3::types::U256 = contract
let balance: U256 = contract
.query("balanceOf", (account,), None, Default::default(), None)
.await?;
Ok(balance.to_string())
}
async fn token_transfer(
from_str: &str,
to_str: &str,
amount_str: &str,
c_str: &str,
secret: &SecretKey,
network: &Network,
chain: &ChainToken,
) -> Result<String> {
let from: EthAddress = from_str.parse()?;
let to: EthAddress = to_str.parse()?;
let amount = U256::from_dec_str(amount_str)?;
let node = network.node();
let transport = web3::transports::Http::new(node)?;
let web3 = Web3::new(transport);
let tx = match chain {
ChainToken::ERC20 => {
let addr: EthAddress = c_str.parse()?;
let contract = Contract::from_json(web3.eth(), addr, ERC20_ABI.as_bytes())?;
let fn_data = contract
.abi()
.function("transfer")
.and_then(|function| function.encode_input(&(to, amount).into_tokens()))?;
TransactionParameters {
to: Some(addr),
data: Bytes(fn_data),
..Default::default()
}
}
ChainToken::ERC721 => {
let addr: EthAddress = c_str.parse()?;
let contract = Contract::from_json(web3.eth(), addr, ERC721_ABI.as_bytes())?;
let fn_data = contract
.abi()
.function("safeTransferFrom")
.and_then(|function| function.encode_input(&(from, to, amount).into_tokens()))?;
TransactionParameters {
to: Some(addr),
data: Bytes(fn_data),
..Default::default()
}
}
ChainToken::ETH => TransactionParameters {
to: Some(to),
value: amount,
..Default::default()
},
_ => return Err(anyhow!("not supported")),
};
let signed = web3.accounts().sign_transaction(tx, secret).await?;
let result = web3
.eth()
.send_raw_transaction(signed.raw_transaction)
.await?;
Ok(format!("{:?}", result))
}
async fn token_gas(
from_str: &str,
to_str: &str,
amount_str: &str,
c_str: &str,
network: &Network,
chain: &ChainToken,
) -> Result<(String, String)> {
let from: EthAddress = from_str.parse()?;
let to: EthAddress = to_str.parse()?;
let amount = U256::from_dec_str(amount_str)?;
let node = network.node();
let transport = web3::transports::Http::new(node)?;
let web3 = Web3::new(transport);
let price = web3.eth().gas_price().await?;
let gas = match chain {
ChainToken::ERC20 => {
let addr: EthAddress = c_str.parse()?;
let contract = Contract::from_json(web3.eth(), addr, ERC20_ABI.as_bytes())?;
let gas = contract
.estimate_gas("transfer", (to, amount), from, Default::default())
.await?;
gas * price
}
ChainToken::ERC721 => {
let addr: EthAddress = c_str.parse()?;
let contract = Contract::from_json(web3.eth(), addr, ERC721_ABI.as_bytes())?;
let gas = contract
.estimate_gas(
"safeTransferFrom",
(from, to, amount),
from,
Default::default(),
)
.await?;
gas * price
}
ChainToken::ETH => {
let tx = CallRequest {
to: Some(to),
value: Some(amount),
..Default::default()
};
let gas = web3.eth().estimate_gas(tx, None).await?;
price * gas
}
_ => return Err(anyhow!("not supported")),
};
Ok((price.to_string(), gas.to_string()))
}
pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) {
handler.add_method("wallet-echo", |_, params, _| async move {
Ok(HandleResult::rpc(json!(params)))
@ -295,4 +418,94 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) { @@ -295,4 +418,94 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) {
Ok(HandleResult::new())
},
);
handler.add_method(
"wallet-gas-price",
|_gid: GroupId, params: Vec<RpcParam>, _state: Arc<RpcState>| 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 from = params[2].as_str().ok_or(RpcError::ParseError)?;
let to = params[3].as_str().ok_or(RpcError::ParseError)?;
let amount = params[4].as_str().ok_or(RpcError::ParseError)?;
let c_str = params[5].as_str().ok_or(RpcError::ParseError)?;
let (price, gas) = token_gas(from, to, amount, c_str, &network, &chain).await?;
Ok(HandleResult::rpc(json!([price, gas])))
},
);
handler.add_method(
"wallet-transfer",
|gid: GroupId, params: Vec<RpcParam>, state: Arc<RpcState>| 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 from = params[2].as_i64().ok_or(RpcError::ParseError)?;
let to = params[3].as_str().ok_or(RpcError::ParseError)?;
let amount = params[4].as_str().ok_or(RpcError::ParseError)?;
let c_str = params[5].as_str().ok_or(RpcError::ParseError)?;
let lock = params[6].as_str().ok_or(RpcError::ParseError)?;
let group_lock = state.group.read().await;
if !group_lock.check_lock(&gid, &lock) {
return Err(RpcError::Custom("Lock is invalid!".to_owned()));
}
let db = wallet_db(group_lock.base(), &gid)?;
let address = Address::get(&db, &from)?;
let (mnemonic, pbytes) = if address.is_gen() {
(group_lock.mnemonic(&gid, lock)?, vec![])
} else {
let pbytes = decrypt(
&group_lock.secret(),
WALLET_DEFAULT_PIN,
address.secret.as_ref(),
)?;
(String::new(), pbytes)
};
let account = group_lock.account(&gid)?;
let lang = account.lang();
let pass = account.pass.to_string();
let account_index = account.index as u32;
drop(group_lock);
let pass = if pass.len() > 0 {
Some(pass.as_ref())
} else {
None
};
let sk: SecretKey = if address.is_gen() {
match chain {
ChainToken::ETH | ChainToken::ERC20 | ChainToken::ERC721 => {
generate_eth_account(
lang,
&mnemonic,
account_index,
address.index as u32,
pass,
)?
}
ChainToken::BTC => {
todo!();
}
}
} else {
let sk = SecretKey::from_slice(&pbytes)
.or(Err(RpcError::Custom("Secret is invalid!".to_owned())))?;
if format!("{:?}", (&sk).address()) != address.address {
return Err(RpcError::Custom("Secret is invalid!".to_owned()));
}
sk
};
let hash = token_transfer(&address.address, to, amount, c_str, &sk, &network, &chain)
.await
.map_err(|e| RpcError::Custom(format!("{:?}", e)))?;
Ok(HandleResult::rpc(json!([
from,
network.to_i64(),
[hash, to],
])))
},
);
}

Loading…
Cancel
Save