Browse Source

Wallet: NFT add & list & transfer supported

pull/18/head
Sun 4 years ago
parent
commit
9904948425
  1. 28
      lib/apps/wallet/models.dart
  2. 251
      lib/apps/wallet/page.dart
  3. 6
      lib/utils/better_print.dart
  4. 6
      lib/widgets/input_text.dart
  5. 28
      src/apps/wallet/models.rs
  6. 21
      src/apps/wallet/rpc.rs

28
lib/apps/wallet/models.dart

@ -101,25 +101,33 @@ extension NetworkExtension on Network { @@ -101,25 +101,33 @@ extension NetworkExtension on Network {
}
}
String txUrl() {
String url() {
switch (this) {
case Network.EthMain:
return 'https://etherscan.io/tx/';
return 'https://etherscan.io/';
case Network.EthTestRopsten:
return 'https://ropsten.etherscan.io/tx/';
return 'https://ropsten.etherscan.io/';
case Network.EthTestRinkeby:
return 'https://rinkeby.etherscan.io/tx/';
return 'https://rinkeby.etherscan.io/';
case Network.EthTestKovan:
return 'https://kovan.etherscan.io/tx/';
return 'https://kovan.etherscan.io/';
case Network.EthLocal:
return 'https://etherscan.io/tx/';
return 'https://etherscan.io/';
case Network.BtcMain:
return 'https://www.blockchain.com/btc/tx/';
return 'https://www.blockchain.com/btc/';
case Network.BtcLocal:
return 'https://www.blockchain.com/btc/tx/';
return 'https://www.blockchain.com/btc/';
}
}
String txUrl() {
return this.url() + '/tx/';
}
String tokenUrl() {
return this.url() + '/token/';
}
int toInt() {
switch (this) {
case Network.EthMain:
@ -305,6 +313,10 @@ class Token { @@ -305,6 +313,10 @@ class Token {
}
}
String nftUrl(String hash) {
return this.network.tokenUrl() + this.contract + '?a=' + hash;
}
Token.fromList(List params, String balance) {
this.id = params[0];
this.chain = ChainTokenExtension.fromInt(params[1]);

251
lib/apps/wallet/page.dart

@ -485,17 +485,31 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt @@ -485,17 +485,31 @@ class _WalletDetailState extends State<WalletDetail> with SingleTickerProviderSt
),
title: Text("${token.balance} ${token.name}",),
subtitle: Text(token.short()),
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
),
),
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
),
),
]
)
);
}
}
@ -692,6 +706,7 @@ class _TransferTokenState extends State<_TransferToken> { @@ -692,6 +706,7 @@ class _TransferTokenState extends State<_TransferToken> {
String _gas = '0';
String _networkError = '';
bool _checked = false;
bool _checking = false;
List<String> _nft = [];
String _selectNft = '-';
@ -703,11 +718,13 @@ class _TransferTokenState extends State<_TransferToken> { @@ -703,11 +718,13 @@ class _TransferTokenState extends State<_TransferToken> {
_nameController.addListener(() {
setState(() {
this._checked = false;
this._checking = false;
});
});
_amountController.addListener(() {
setState(() {
this._checked = false;
this._checking = false;
});
});
if (widget.token.isNft()) {
@ -722,9 +739,12 @@ class _TransferTokenState extends State<_TransferToken> { @@ -722,9 +739,12 @@ class _TransferTokenState extends State<_TransferToken> {
if (res.isOk) {
final a = res.params[0];
final t = res.params[1];
res.params[2].forEach((hash) {
this._nft.add(hash);
});
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;
@ -751,7 +771,7 @@ class _TransferTokenState extends State<_TransferToken> { @@ -751,7 +771,7 @@ class _TransferTokenState extends State<_TransferToken> {
} else {
this._networkError = res.error;
}
this._checking = false;
setState(() {});
}
@ -911,36 +931,36 @@ class _TransferTokenState extends State<_TransferToken> { @@ -911,36 +931,36 @@ class _TransferTokenState extends State<_TransferToken> {
child: this._nftInput
? InputText(
icon: Icons.verified,
text: '0xAa..',
text: 'TokenID',
controller: _amountController,
focus: _amountFocus)
: 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(value, maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 16)
),
);
}).toList(),
: 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(gidPrint(value, '', 6)),
);
}).toList(),
),
),
),
)
)
),
SizedBox(width: 80.0,
@ -987,8 +1007,11 @@ class _TransferTokenState extends State<_TransferToken> { @@ -987,8 +1007,11 @@ class _TransferTokenState extends State<_TransferToken> {
if (_myAccount) {
to = this._selectAddress;
}
final a = _amountController.text.trim();
if (double.parse(a) == 0) {
String a = _amountController.text.trim();
if (!_nftInput) {
a = this._selectNft;
}
if (a.length == 0 || (!widget.token.isNft() && double.parse(a) == 0)) {
_amountFocus.requestFocus();
return;
}
@ -1017,14 +1040,18 @@ class _TransferTokenState extends State<_TransferToken> { @@ -1017,14 +1040,18 @@ class _TransferTokenState extends State<_TransferToken> {
);
})
: ButtonText(
text: lang.check,
enable: !this._checking,
text: this._checking ? lang.waiting : 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) {
String a = _amountController.text.trim();
if (!_nftInput) {
a = this._selectNft;
}
if (a.length == 0 || (!widget.token.isNft() && double.parse(a) == 0)) {
_amountFocus.requestFocus();
return;
}
@ -1034,8 +1061,138 @@ class _TransferTokenState extends State<_TransferToken> { @@ -1034,8 +1061,138 @@ class _TransferTokenState extends State<_TransferToken> {
}
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(Global.httpRpc, '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(Global.httpRpc, '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: ' + gidPrint(hash, '', 6)),
trailing: IconButton(icon: Icon(Icons.link, color: color.primary),
onPressed: () {
launch(widget.token.nftUrl(hash));
}
),
);
}
),
)
]
);
}
}

6
lib/utils/better_print.dart

@ -5,15 +5,15 @@ String gidText(String? gid, [String pre='EH']) { @@ -5,15 +5,15 @@ String gidText(String? gid, [String pre='EH']) {
return pre + gid.toUpperCase();
}
String gidPrint(String? gid, [String pre='EH']) {
String gidPrint(String? gid, [String pre='EH', int n = 4]) {
if (gid == null) {
return '';
}
final info = gid.toUpperCase();
final len = info.length;
if (len > 8) {
return pre + info.substring(0, 4) + '...' + info.substring(len - 4, len);
if (len > n+n) {
return pre + info.substring(0, n) + '...' + info.substring(len - n, len);
} else {
return info;
}

6
lib/widgets/input_text.dart

@ -5,8 +5,9 @@ class InputText extends StatelessWidget { @@ -5,8 +5,9 @@ class InputText extends StatelessWidget {
final String text;
final TextEditingController controller;
final FocusNode focus;
final bool enabled;
const InputText({Key? key, required this.icon, required this.text, required this.controller, required this.focus})
const InputText({Key? key, required this.icon, required this.text, required this.controller, required this.focus, this.enabled = true})
: super(key: key);
@override
Widget build(BuildContext context) {
@ -17,7 +18,7 @@ class InputText extends StatelessWidget { @@ -17,7 +18,7 @@ class InputText extends StatelessWidget {
height: 50.0,
width: 600.0,
decoration: BoxDecoration(
color: color.surface,
color: enabled ? color.surface : Color(0x26ADB0BB),
border: Border.all(color: focus.hasFocus ? color.primary : color.surface),
borderRadius: BorderRadius.circular(10.0)
),
@ -33,6 +34,7 @@ class InputText extends StatelessWidget { @@ -33,6 +34,7 @@ class InputText extends StatelessWidget {
)),
Expanded(
child: TextField(
enabled: enabled,
style: TextStyle(fontSize: 16.0),
controller: controller,
focusNode: focus,

28
src/apps/wallet/models.rs

@ -266,6 +266,18 @@ impl Address { @@ -266,6 +266,18 @@ impl Address {
Err(anyhow!("address is missing!"))
}
pub fn get_by_address(db: &DStorage, address: &str) -> Result<Self> {
let mut matrix = db.query(&format!(
"SELECT id, chain, indx, name, address, secret, balance FROM addresses WHERE address = '{}'",
address
))?;
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",
@ -410,6 +422,18 @@ impl Token { @@ -410,6 +422,18 @@ impl Token {
Err(anyhow!("token is missing!"))
}
pub fn get_by_contract(db: &DStorage, network: &Network, c: &str) -> Result<Self> {
let mut matrix = db.query(&format!(
"SELECT id, chain, network, name, contract, decimal FROM tokens WHERE network = {} AND contract = '{}'",
network.to_i64(), c
))?;
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)?;
@ -519,8 +543,8 @@ impl Balance { @@ -519,8 +543,8 @@ impl Balance {
Err(anyhow!("balance is missing!"))
}
pub fn _delete(db: &DStorage, id: &i64) -> Result<()> {
let sql = format!("DELETE FROM balances WHERE id = {}", id);
pub fn delete_by_hash(db: &DStorage, hash: &str) -> Result<()> {
let sql = format!("DELETE FROM balances WHERE value = '{}'", hash);
db.delete(&sql)?;
Ok(())
}

21
src/apps/wallet/rpc.rs

@ -107,6 +107,10 @@ async fn loop_token( @@ -107,6 +107,10 @@ async fn loop_token(
let balance =
token_balance(&web3, &token.contract, &address, &token.chain).await?;
let res = res_balance(gid, &address, &network, &balance, Some(&token));
// update & clean balances.
// TODO
sender.send(SendMessage::Rpc(0, res, true)).await?;
}
}
@ -306,7 +310,11 @@ async fn token_gas( @@ -306,7 +310,11 @@ async fn token_gas(
async fn nft_check(node: &str, c_str: &str, hash: &str) -> Result<String> {
let addr: EthAddress = c_str.parse()?;
let tokenid = U256::from_dec_str(&hash)?;
let tokenid = if hash.starts_with("0x") {
U256::from_str_radix(&hash, 16)?
} else {
U256::from_dec_str(&hash)?
};
let transport = Http::new(node)?;
let web3 = Web3::new(transport);
let contract = Contract::from_json(web3.eth(), addr, ERC721_ABI.as_bytes())?;
@ -517,6 +525,17 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) { @@ -517,6 +525,17 @@ pub(crate) fn new_rpc_handler(handler: &mut RpcHandler<RpcState>) {
.await
.map_err(|e| RpcError::Custom(format!("{:?}", e)))?;
// NFT: delete old, add new if needed (between accounts).
if let Ok(token) = Token::get_by_contract(&db, &network, c_str) {
if token.chain == ChainToken::ERC721 {
let _ = Balance::delete_by_hash(&db, amount);
if let Ok(new) = Address::get_by_address(&db, to) {
let _ = Balance::add(&db, new.id, token.id, amount.to_owned());
}
}
}
Ok(HandleResult::rpc(json!([
from,
network.to_i64(),

Loading…
Cancel
Save