remove fees from send screen, send and receive transactions working

This commit is contained in:
fosse 2023-08-01 09:50:07 -04:00
parent 3a50b58b09
commit 92d4a6f6d6
8 changed files with 289 additions and 117 deletions

View file

@ -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();

View file

@ -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);

View file

@ -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,

View file

@ -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) {

View file

@ -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) {

View file

@ -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),

View file

@ -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;

View file

@ -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 '';
}