mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-01-11 05:14:46 +00:00
remove fees from send screen, send and receive transactions working
This commit is contained in:
parent
3a50b58b09
commit
92d4a6f6d6
8 changed files with 289 additions and 117 deletions
|
@ -8,18 +8,18 @@ import 'package:http/io_client.dart' as ioc;
|
|||
|
||||
part 'node.g.dart';
|
||||
|
||||
Uri createUriFromElectrumAddress(String address) =>
|
||||
Uri.tryParse('tcp://$address')!;
|
||||
Uri createUriFromElectrumAddress(String address) => Uri.tryParse('tcp://$address')!;
|
||||
|
||||
@HiveType(typeId: Node.typeId)
|
||||
class Node extends HiveObject with Keyable {
|
||||
Node(
|
||||
{this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.trusted = false,
|
||||
String? uri,
|
||||
WalletType? type,}) {
|
||||
Node({
|
||||
this.login,
|
||||
this.password,
|
||||
this.useSSL,
|
||||
this.trusted = false,
|
||||
String? uri,
|
||||
WalletType? type,
|
||||
}) {
|
||||
if (uri != null) {
|
||||
uriRaw = uri;
|
||||
}
|
||||
|
@ -71,7 +71,12 @@ class Node extends HiveObject with Keyable {
|
|||
case WalletType.ethereum:
|
||||
return Uri.https(uriRaw, '');
|
||||
case WalletType.nano:
|
||||
return Uri.https(uriRaw, '');
|
||||
case WalletType.banano:
|
||||
if (uriRaw.contains("https") || uriRaw.endsWith("443") || isSSL) {
|
||||
return Uri.https(uriRaw, '');
|
||||
} else {
|
||||
return Uri.http(uriRaw, '');
|
||||
}
|
||||
default:
|
||||
throw Exception('Unexpected type ${type.toString()} for Node uri');
|
||||
}
|
||||
|
@ -80,12 +85,12 @@ class Node extends HiveObject with Keyable {
|
|||
@override
|
||||
bool operator ==(other) =>
|
||||
other is Node &&
|
||||
(other.uriRaw == uriRaw &&
|
||||
other.login == login &&
|
||||
other.password == password &&
|
||||
other.typeRaw == typeRaw &&
|
||||
other.useSSL == useSSL &&
|
||||
other.trusted == trusted);
|
||||
(other.uriRaw == uriRaw &&
|
||||
other.login == login &&
|
||||
other.password == password &&
|
||||
other.typeRaw == typeRaw &&
|
||||
other.useSSL == useSSL &&
|
||||
other.trusted == trusted);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
|
@ -133,27 +138,23 @@ class Node extends HiveObject with Keyable {
|
|||
final path = '/json_rpc';
|
||||
final rpcUri = isSSL ? Uri.https(uri.authority, path) : Uri.http(uri.authority, path);
|
||||
final realm = 'monero-rpc';
|
||||
final body = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': '0',
|
||||
'method': 'get_info'
|
||||
};
|
||||
final body = {'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'};
|
||||
|
||||
try {
|
||||
final authenticatingClient = HttpClient();
|
||||
|
||||
authenticatingClient.addCredentials(
|
||||
rpcUri,
|
||||
realm,
|
||||
HttpClientDigestCredentials(login ?? '', password ?? ''),
|
||||
rpcUri,
|
||||
realm,
|
||||
HttpClientDigestCredentials(login ?? '', password ?? ''),
|
||||
);
|
||||
|
||||
final http.Client client = ioc.IOClient(authenticatingClient);
|
||||
|
||||
final response = await client.post(
|
||||
rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode(body),
|
||||
rpcUri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: json.encode(body),
|
||||
);
|
||||
|
||||
client.close();
|
||||
|
|
|
@ -20,7 +20,6 @@ class NanoClient {
|
|||
static const String DEFAULT_REPRESENTATIVE =
|
||||
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
|
||||
|
||||
// final _httpClient = http.Client();
|
||||
StreamSubscription<Transfer>? subscription;
|
||||
Node? _node;
|
||||
|
||||
|
@ -195,6 +194,160 @@ class NanoClient {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> receiveBlock({
|
||||
required String blockHash,
|
||||
required String source,
|
||||
required String amountRaw,
|
||||
required String destinationAddress,
|
||||
required String privateKey,
|
||||
}) async {
|
||||
bool openBlock = false;
|
||||
|
||||
final headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
// first check if the account is open:
|
||||
// get the account info (we need the frontier and representative):
|
||||
final infoBody = jsonEncode({
|
||||
"action": "account_info",
|
||||
"representative": "true",
|
||||
"account": destinationAddress,
|
||||
});
|
||||
final infoResponse = await http.post(
|
||||
_node!.uri,
|
||||
headers: headers,
|
||||
body: infoBody,
|
||||
);
|
||||
final infoData = jsonDecode(infoResponse.body);
|
||||
|
||||
if (infoData["error"] != null) {
|
||||
// account is not open yet, we need to create an open block:
|
||||
openBlock = true;
|
||||
}
|
||||
|
||||
// first get the account balance:
|
||||
final balanceBody = jsonEncode({
|
||||
"action": "account_balance",
|
||||
"account": destinationAddress,
|
||||
});
|
||||
|
||||
final balanceResponse = await http.post(
|
||||
_node!.uri,
|
||||
headers: headers,
|
||||
body: balanceBody,
|
||||
);
|
||||
|
||||
final balanceData = jsonDecode(balanceResponse.body);
|
||||
final BigInt currentBalance = BigInt.parse(balanceData["balance"].toString());
|
||||
final BigInt txAmount = BigInt.parse(amountRaw);
|
||||
final BigInt balanceAfterTx = currentBalance + txAmount;
|
||||
|
||||
String frontier = infoData["frontier"].toString();
|
||||
String representative = infoData["representative"].toString();
|
||||
|
||||
if (openBlock) {
|
||||
// we don't have a representative set yet:
|
||||
representative = DEFAULT_REPRESENTATIVE;
|
||||
}
|
||||
|
||||
// link = send block hash:
|
||||
final String link = blockHash;
|
||||
// this "linkAsAccount" is meaningless:
|
||||
final String linkAsAccount = NanoAccounts.createAccount(NanoAccountType.NANO, blockHash);
|
||||
|
||||
// construct the receive block:
|
||||
Map<String, String> receiveBlock = {
|
||||
"type": "state",
|
||||
"account": destinationAddress,
|
||||
"previous":
|
||||
openBlock ? "0000000000000000000000000000000000000000000000000000000000000000" : frontier,
|
||||
"representative": representative,
|
||||
"balance": balanceAfterTx.toString(),
|
||||
"link": link,
|
||||
"link_as_account": linkAsAccount,
|
||||
};
|
||||
|
||||
// sign the receive block:
|
||||
final String hash = NanoBlocks.computeStateHash(
|
||||
NanoAccountType.NANO,
|
||||
receiveBlock["account"]!,
|
||||
receiveBlock["previous"]!,
|
||||
receiveBlock["representative"]!,
|
||||
BigInt.parse(receiveBlock["balance"]!),
|
||||
receiveBlock["link"]!,
|
||||
);
|
||||
final String signature = NanoSignatures.signBlock(hash, privateKey);
|
||||
|
||||
// get PoW for the receive block:
|
||||
String? work;
|
||||
if (openBlock) {
|
||||
work = await requestWork(NanoAccounts.extractPublicKey(destinationAddress));
|
||||
} else {
|
||||
work = await requestWork(frontier);
|
||||
}
|
||||
receiveBlock["link_as_account"] = linkAsAccount;
|
||||
receiveBlock["signature"] = signature;
|
||||
receiveBlock["work"] = work;
|
||||
|
||||
// process the receive block:
|
||||
|
||||
final processBody = jsonEncode({
|
||||
"action": "process",
|
||||
"json_block": "true",
|
||||
"subtype": "receive",
|
||||
"block": receiveBlock,
|
||||
});
|
||||
final processResponse = await http.post(
|
||||
_node!.uri,
|
||||
headers: headers,
|
||||
body: processBody,
|
||||
);
|
||||
|
||||
final Map<String, dynamic> decoded = json.decode(processResponse.body) as Map<String, dynamic>;
|
||||
if (decoded.containsKey("error")) {
|
||||
throw Exception("Received error ${decoded["error"]}");
|
||||
}
|
||||
}
|
||||
|
||||
// returns the number of blocks received:
|
||||
Future<int> confirmAllReceivable({
|
||||
required String destinationAddress,
|
||||
required String privateKey,
|
||||
}) async {
|
||||
final receivableResponse = await http.post(_node!.uri,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({
|
||||
"action": "receivable",
|
||||
"source": "true",
|
||||
"account": destinationAddress,
|
||||
"count": "-1",
|
||||
}));
|
||||
|
||||
final receivableData = await jsonDecode(receivableResponse.body);
|
||||
if (receivableData["blocks"] == "") {
|
||||
return 0;
|
||||
}
|
||||
final blocks = receivableData["blocks"] as Map<String, dynamic>;
|
||||
// confirm all receivable blocks:
|
||||
for (final blockHash in blocks.keys) {
|
||||
final block = blocks[blockHash];
|
||||
final String amountRaw = block["amount"] as String;
|
||||
final String source = block["source"] as String;
|
||||
await receiveBlock(
|
||||
blockHash: blockHash,
|
||||
source: source,
|
||||
amountRaw: amountRaw,
|
||||
privateKey: privateKey,
|
||||
destinationAddress: destinationAddress,
|
||||
);
|
||||
// a bit of a hack:
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
}
|
||||
|
||||
return blocks.keys.length;
|
||||
}
|
||||
|
||||
Future<dynamic> getTransactionDetails(String transactionHash) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
@ -219,8 +372,7 @@ class NanoClient {
|
|||
// Map the transactions list to NanoTransactionModel using the factory
|
||||
// reversed so that the DateTime is correct when local_timestamp is absent
|
||||
return transactions.reversed
|
||||
.map<NanoTransactionModel>(
|
||||
(transaction) => NanoTransactionModel.fromJson(transaction as Map<String, dynamic>))
|
||||
.map<NanoTransactionModel>((transaction) => NanoTransactionModel.fromJson(transaction))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
|
|
|
@ -17,17 +17,17 @@ class NanoTransactionModel {
|
|||
required this.account,
|
||||
});
|
||||
|
||||
factory NanoTransactionModel.fromJson(Map<String, dynamic> json) {
|
||||
DateTime? local_timestamp;
|
||||
factory NanoTransactionModel.fromJson(dynamic json) {
|
||||
DateTime? localTimestamp;
|
||||
try {
|
||||
local_timestamp =
|
||||
DateTime.fromMillisecondsSinceEpoch(int.parse(json["local_timeStamp"] as String) * 1000);
|
||||
localTimestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||
int.parse(json["local_timestamp"] as String) * 1000);
|
||||
} catch (e) {
|
||||
local_timestamp = DateTime.now();
|
||||
localTimestamp = DateTime.now();
|
||||
}
|
||||
|
||||
return NanoTransactionModel(
|
||||
date: local_timestamp,
|
||||
date: localTimestamp,
|
||||
hash: json["hash"] as String,
|
||||
height: int.parse(json["height"] as String),
|
||||
type: json["type"] as String,
|
||||
|
|
|
@ -117,7 +117,8 @@ abstract class NanoWalletBase
|
|||
throw Exception("Ethereum Node connection failed");
|
||||
}
|
||||
// _client.setListeners(_privateKey.address, _onNewTransaction);
|
||||
_updateBalance();
|
||||
await _updateBalance();
|
||||
await _receiveAll();
|
||||
syncStatus = ConnectedSyncStatus();
|
||||
} catch (e) {
|
||||
syncStatus = FailedSyncStatus();
|
||||
|
@ -184,6 +185,19 @@ abstract class NanoWalletBase
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _receiveAll() async {
|
||||
int blocksReceived = await this._client.confirmAllReceivable(
|
||||
destinationAddress: _publicAddress,
|
||||
privateKey: _privateKey,
|
||||
);
|
||||
|
||||
if (blocksReceived > 0) {
|
||||
await Future<void>.delayed(Duration(seconds: 3));
|
||||
_updateBalance();
|
||||
updateTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTransactions() async {
|
||||
try {
|
||||
if (_isTransactionUpdating) {
|
||||
|
@ -255,6 +269,10 @@ abstract class NanoWalletBase
|
|||
syncStatus = AttemptingSyncStatus();
|
||||
await _updateBalance();
|
||||
await updateTransactions();
|
||||
Timer.periodic(
|
||||
const Duration(minutes: 1),
|
||||
(timer) async => await _receiveAll(),
|
||||
);
|
||||
|
||||
syncStatus = SyncedSyncStatus();
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
part of 'nano.dart';
|
||||
|
||||
class CWNano extends Nano {
|
||||
// @override
|
||||
// NanoAccountList getAccountList(Object wallet) {
|
||||
// return CWNanoAccountList(wallet);
|
||||
// }
|
||||
|
||||
@override
|
||||
List<String> getNanoWordList(String language) {
|
||||
// throw UnimplementedError();
|
||||
return NanoMnemomics.WORDLIST;
|
||||
}
|
||||
|
||||
@override
|
||||
WalletService createNanoWalletService(Box<WalletInfo> walletInfoSource) {
|
||||
print("creating NanoWalletService");
|
||||
return NanoWalletService(walletInfoSource);
|
||||
}
|
||||
|
||||
|
@ -61,15 +54,11 @@ class CWNano extends Nano {
|
|||
|
||||
@override
|
||||
TransactionHistoryBase getTransactionHistory(Object wallet) {
|
||||
// final moneroWallet = wallet as MoneroWallet;
|
||||
// return moneroWallet.transactionHistory;
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void onStartup() {
|
||||
// monero_wallet_api.onStartup();
|
||||
}
|
||||
void onStartup() {}
|
||||
|
||||
@override
|
||||
Object createNanoTransactionCredentials(List<Output> outputs) {
|
||||
|
|
|
@ -449,78 +449,79 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
|||
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => GestureDetector(
|
||||
onTap: () => _setTransactionPriority(context),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).send_estimated_fee,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color: Colors.white),
|
||||
),
|
||||
Container(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
output.estimatedFee.toString() +
|
||||
' ' +
|
||||
sendViewModel.selectedCryptoCurrency.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: sendViewModel.isFiatDisabled
|
||||
? const SizedBox(height: 14)
|
||||
: Text(
|
||||
output.estimatedFeeFiatAmount +
|
||||
' ' +
|
||||
sendViewModel.fiat.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 2, left: 5),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
if (sendViewModel.hasFees)
|
||||
Observer(
|
||||
builder: (_) => GestureDetector(
|
||||
onTap: () => _setTransactionPriority(context),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).send_estimated_fee,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color: Colors.white),
|
||||
),
|
||||
)
|
||||
],
|
||||
Container(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
output.estimatedFee.toString() +
|
||||
' ' +
|
||||
sendViewModel.selectedCryptoCurrency.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: sendViewModel.isFiatDisabled
|
||||
? const SizedBox(height: 14)
|
||||
: Text(
|
||||
output.estimatedFeeFiatAmount +
|
||||
' ' +
|
||||
sendViewModel.fiat.title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context)
|
||||
.primaryTextTheme
|
||||
.headlineSmall!
|
||||
.decorationColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 2, left: 5),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (sendViewModel.isElectrumWallet)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
|
|
|
@ -165,6 +165,9 @@ abstract class SendViewModelBase with Store {
|
|||
bool get isElectrumWallet =>
|
||||
_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin;
|
||||
|
||||
@computed
|
||||
bool get hasFees => _wallet.type != WalletType.nano && _wallet.type != WalletType.banano;
|
||||
|
||||
@observable
|
||||
CryptoCurrency selectedCryptoCurrency;
|
||||
|
||||
|
|
|
@ -116,6 +116,10 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
return 'https://explorer.havenprotocol.org/search?value=${txId}';
|
||||
case WalletType.ethereum:
|
||||
return 'https://etherscan.io/tx/${txId}';
|
||||
case WalletType.nano:
|
||||
return 'https://nanolooker.com/block/${txId}';
|
||||
case WalletType.banano:
|
||||
return 'https://bananolooker.com/block/${txId}';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -133,6 +137,10 @@ abstract class TransactionDetailsViewModelBase with Store {
|
|||
return S.current.view_transaction_on + 'explorer.havenprotocol.org';
|
||||
case WalletType.ethereum:
|
||||
return S.current.view_transaction_on + 'etherscan.io';
|
||||
case WalletType.nano:
|
||||
return S.current.view_transaction_on + 'nanolooker.com';
|
||||
case WalletType.banano:
|
||||
return S.current.view_transaction_on + 'bananolooker.com';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue