Minor nano fixes + abstract nano utils out into package (#1250)

* fixes

* refactors

* fixes

* more fixes

* fixes for monero.com

* update confirmed balance to receivable balance

* fix balance display bug

* remove print statements

* prevent unnecessary writes

* review fixes

* forgot to add pubspec changes

* fix

* fix sync status desyncing on rpc fail

* fix

* update translations + txDetails fixes

* squash balance bug

* add source address on tx info

* fix
This commit is contained in:
Matthew Fosse 2024-01-11 15:00:41 -08:00 committed by GitHub
parent 8d973cf919
commit fe2e26f146
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 306 additions and 518 deletions

View file

@ -1,20 +1,19 @@
import 'package:cw_core/balance.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:nanoutil/nanoutil.dart';
class BananoBalance extends Balance {
final BigInt currentBalance;
final BigInt receivableBalance;
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) {
}
BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
@override
String get formattedAvailableBalance {
return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano);
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano);
}
@override
String get formattedAdditionalBalance {
return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerBanano);
return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano);
}
}

View file

@ -1,34 +1,35 @@
import 'package:cw_core/balance.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:nanoutil/nanoutil.dart';
BigInt stringAmountToBigInt(String amount) {
return BigInt.parse(NanoUtil.getAmountAsRaw(amount, NanoUtil.rawPerNano));
return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano));
}
class NanoBalance extends Balance {
final BigInt currentBalance;
final BigInt receivableBalance;
late String formattedCurrentBalance;
late String formattedReceivableBalance;
NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) {
this.formattedCurrentBalance = "";
this.formattedReceivableBalance = "";
}
NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0);
NanoBalance.fromString(
{required this.formattedCurrentBalance, required this.formattedReceivableBalance})
NanoBalance.fromFormattedString(
{required String formattedCurrentBalance, required String formattedReceivableBalance})
: currentBalance = stringAmountToBigInt(formattedCurrentBalance),
receivableBalance = stringAmountToBigInt(formattedReceivableBalance),
super(0, 0);
NanoBalance.fromRawString(
{required String currentBalance, required String receivableBalance})
: currentBalance = BigInt.parse(currentBalance),
receivableBalance = BigInt.parse(receivableBalance),
super(0, 0);
@override
String get formattedAvailableBalance {
return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerNano);
return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano);
}
@override
String get formattedAdditionalBalance {
return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerNano);
return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano);
}
}

View file

@ -4,10 +4,10 @@ import 'dart:convert';
import 'package:cw_core/nano_account_info_response.dart';
import 'package:cw_nano/nano_balance.dart';
import 'package:cw_nano/nano_transaction_model.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:http/http.dart' as http;
import 'package:nanodart/nanodart.dart';
import 'package:cw_core/node.dart';
import 'package:nanoutil/nanoutil.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NanoClient {
@ -61,6 +61,13 @@ class NanoClient {
),
);
final data = await jsonDecode(response.body);
if (response.statusCode != 200 ||
data["error"] != null ||
data["balance"] == null ||
data["receivable"] == null) {
throw Exception(
"Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}");
}
final String currentBalance = data["balance"] as String;
final String receivableBalance = data["receivable"] as String;
final BigInt cur = BigInt.parse(currentBalance);
@ -203,7 +210,7 @@ class NanoClient {
String? previousHash,
}) async {
// our address:
final String publicAddress = NanoUtil.privateKeyToAddress(privateKey);
final String publicAddress = NanoDerivations.privateKeyToAddress(privateKey);
// first get the current account balance:
if (balanceAfterTx == null) {

View file

@ -1,7 +1,7 @@
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:nanoutil/nanoutil.dart';
class NanoTransactionInfo extends TransactionInfo {
NanoTransactionInfo({
@ -13,6 +13,8 @@ class NanoTransactionInfo extends TransactionInfo {
required this.confirmed,
required this.date,
required this.confirmations,
required this.to,
required this.from,
}) : this.amount = amountRaw.toInt();
final String id;
@ -24,14 +26,17 @@ class NanoTransactionInfo extends TransactionInfo {
final bool confirmed;
final int confirmations;
final String tokenSymbol;
final String? to;
final String? from;
String? _fiatAmount;
bool get isPending => !this.confirmed;
@override
String amountFormatted() {
final String amt = NanoUtil.getRawAsUsableString(amountRaw.toString(), NanoUtil.rawPerNano);
final String acc = NanoUtil.getRawAccuracy(amountRaw.toString(), NanoUtil.rawPerNano);
final String amt =
NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano);
final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano);
return "$acc$amt $tokenSymbol";
}
@ -54,6 +59,8 @@ class NanoTransactionInfo extends TransactionInfo {
confirmed: data['confirmed'] as bool,
confirmations: data['confirmations'] as int,
tokenSymbol: data['tokenSymbol'] as String,
to: data['to'] as String,
from: data['from'] as String,
);
}
@ -66,5 +73,7 @@ class NanoTransactionInfo extends TransactionInfo {
'confirmed': confirmed,
'confirmations': confirmations,
'tokenSymbol': tokenSymbol,
'to': to,
'from': from,
};
}

View file

@ -1,193 +0,0 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:convert/convert.dart';
import "package:ed25519_hd_key/ed25519_hd_key.dart";
import 'package:libcrypto/libcrypto.dart';
import 'package:nanodart/nanodart.dart';
import 'package:decimal/decimal.dart';
class NanoUtil {
// standard:
static String seedToPrivate(String seed, int index) {
return NanoKeys.seedToPrivate(seed, index);
}
static String seedToAddress(String seed, int index) {
return NanoAccounts.createAccount(
NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index)));
}
static String seedToMnemonic(String seed) {
return NanoMnemomics.seedToMnemonic(seed).join(" ");
}
static Future<String> mnemonicToSeed(String mnemonic) async {
return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' '));
}
static String privateKeyToPublic(String privateKey) {
// return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!);
return NanoKeys.createPublicKey(privateKey);
}
static String addressToPublicKey(String publicAddress) {
return NanoAccounts.extractPublicKey(publicAddress);
}
// universal:
static String privateKeyToAddress(String privateKey) {
return NanoAccounts.createAccount(NanoAccountType.NANO, privateKeyToPublic(privateKey));
}
static String publicKeyToAddress(String publicKey) {
return NanoAccounts.createAccount(NanoAccountType.NANO, publicKey);
}
// standard + hd:
static bool isValidSeed(String seed) {
// Ensure seed is 64 or 128 characters long
if (seed == null || (seed.length != 64 && seed.length != 128)) {
return false;
}
// Ensure seed only contains hex characters, 0-9;A-F
return NanoHelpers.isHexString(seed);
}
// // hd:
static Future<String> hdMnemonicListToSeed(List<String> words) async {
// if (words.length != 24) {
// throw Exception('Expected a 24-word list, got a ${words.length} list');
// }
final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic'));
final Pbkdf2 hasher = Pbkdf2(iterations: 2048);
final String seed = await hasher.sha512(words.join(' '), salt);
return seed;
}
static Future<String> hdSeedToPrivate(String seed, int index) async {
List<int> seedBytes = hex.decode(seed);
KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes);
return hex.encode(data.key);
}
static Future<String> hdSeedToAddress(String seed, int index) async {
return NanoAccounts.createAccount(
NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index)));
}
static Future<String> uniSeedToAddress(String seed, int index, String type) {
if (type == "standard") {
return Future<String>.value(seedToAddress(seed, index));
} else if (type == "hd") {
return hdSeedToAddress(seed, index);
} else {
throw Exception('Unknown seed type');
}
}
static Future<String> uniSeedToPrivate(String seed, int index, String type) {
if (type == "standard") {
return Future<String>.value(seedToPrivate(seed, index));
} else if (type == "hd") {
return hdSeedToPrivate(seed, index);
} else {
throw Exception('Unknown seed type');
}
}
static bool isValidBip39Seed(String seed) {
// Ensure seed is 128 characters long
if (seed.length != 128) {
return false;
}
// Ensure seed only contains hex characters, 0-9;A-F
return NanoHelpers.isHexString(seed);
}
// number util:
static const int maxDecimalDigits = 6; // Max digits after decimal
static BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000");
static BigInt rawPerNyano = BigInt.parse("1000000000000000000000000");
static BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000");
static BigInt rawPerXMR = BigInt.parse("1000000000000");
static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000");
// static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000");
/// Convert raw to ban and return as BigDecimal
///
/// @param raw 100000000000000000000000000000
/// @return Decimal value 1.000000000000000000000000000000
///
static Decimal getRawAsDecimal(String? raw, BigInt? rawPerCur) {
rawPerCur ??= rawPerNano;
final Decimal amount = Decimal.parse(raw.toString());
final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal();
return result;
}
static String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) {
Decimal bigger = input.shift(digits);
bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05
bigger = bigger.shift(-digits);
return bigger.toString();
}
/// Return raw as a NANO amount.
///
/// @param raw 100000000000000000000000000000
/// @returns 1
///
static String getRawAsUsableString(String? raw, BigInt rawPerCur) {
final String res =
truncateDecimal(getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9);
if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") {
return "0";
}
if (!res.contains(".")) {
return res;
}
final String numAmount = res.split(".")[0];
String decAmount = res.split(".")[1];
// truncate:
if (decAmount.length > maxDecimalDigits) {
decAmount = decAmount.substring(0, maxDecimalDigits);
// remove trailing zeros:
decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => '');
if (decAmount.isEmpty) {
return numAmount;
}
}
return "$numAmount.$decAmount";
}
static String getRawAccuracy(String? raw, BigInt rawPerCur) {
final String rawString = getRawAsUsableString(raw, rawPerCur);
final String rawDecimalString = getRawAsDecimal(raw, rawPerCur).toString();
if (raw == null || raw.isEmpty || raw == "0") {
return "";
}
if (rawString != rawDecimalString) {
return "~";
}
return "";
}
/// Return readable string amount as raw string
/// @param amount 1.01
/// @returns 101000000000000000000000000000
///
static String getAmountAsRaw(String amount, BigInt rawPerCur) {
final Decimal asDecimal = Decimal.parse(amount);
final Decimal rawDecimal = Decimal.parse(rawPerCur.toString());
return (asDecimal * rawDecimal).toString();
}
}

View file

@ -18,7 +18,6 @@ import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_transaction_credentials.dart';
import 'package:cw_nano/nano_transaction_history.dart';
import 'package:cw_nano/nano_transaction_info.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:cw_nano/nano_wallet_keys.dart';
import 'package:cw_nano/pending_nano_transaction.dart';
import 'package:mobx/mobx.dart';
@ -27,6 +26,7 @@ import 'package:cw_nano/nano_wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:nanodart/nanodart.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:nanoutil/nanoutil.dart';
part 'nano_wallet.g.dart';
@ -83,6 +83,8 @@ abstract class NanoWalletBase
@observable
late ObservableMap<CryptoCurrency, NanoBalance> balance;
static const int POLL_INTERVAL_SECONDS = 10;
// initialize the different forms of private / public key we'll need:
Future<void> init() async {
if (_derivationType == DerivationType.unknown) {
@ -100,11 +102,21 @@ abstract class NanoWalletBase
if (_derivationType == DerivationType.nano) {
_hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase();
} else {
_hexSeed = await NanoUtil.hdMnemonicListToSeed(_mnemonic.split(' '));
_hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' '));
}
}
_privateKey = await NanoUtil.uniSeedToPrivate(_hexSeed!, 0, type);
_publicAddress = await NanoUtil.uniSeedToAddress(_hexSeed!, 0, type);
NanoDerivationType derivationType =
type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD;
_privateKey = await NanoDerivations.universalSeedToPrivate(
_hexSeed!,
index: 0,
type: derivationType,
);
_publicAddress = await NanoDerivations.universalSeedToAddress(
_hexSeed!,
index: 0,
type: derivationType,
);
this.walletInfo.address = _publicAddress!;
await walletAddresses.init();
@ -125,6 +137,7 @@ abstract class NanoWalletBase
@override
void close() {
_client.stop();
_receiveTimer?.cancel();
}
@action
@ -139,6 +152,7 @@ abstract class NanoWalletBase
try {
await _updateBalance();
await updateTransactions();
await _updateRep();
await _receiveAll();
} catch (e) {
@ -173,8 +187,8 @@ abstract class NanoWalletBase
if (txOut.sendAll) {
amt = balance[currency]?.currentBalance ?? BigInt.zero;
} else {
amt = BigInt.tryParse(NanoUtil.getAmountAsRaw(
txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoUtil.rawPerNano)) ??
amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw(
txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ??
BigInt.zero;
}
@ -186,9 +200,7 @@ abstract class NanoWalletBase
final block = await _client.constructSendBlock(
amountRaw: amt.toString(),
destinationAddress: txOut.isParsedAddress
? txOut.extractedAddress!
: txOut.address,
destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address,
privateKey: _privateKey!,
balanceAfterTx: runningBalance,
previousHash: previousHash,
@ -236,10 +248,10 @@ abstract class NanoWalletBase
}
}
Future<void> updateTransactions() async {
Future<bool> updateTransactions() async {
try {
if (_isTransactionUpdating) {
return;
return false;
}
_isTransactionUpdating = true;
@ -247,8 +259,10 @@ abstract class NanoWalletBase
transactionHistory.addMany(transactions);
await transactionHistory.save();
_isTransactionUpdating = false;
return true;
} catch (_) {
_isTransactionUpdating = false;
return false;
}
}
@ -261,16 +275,17 @@ abstract class NanoWalletBase
final Map<String, NanoTransactionInfo> result = {};
for (var transactionModel in transactions) {
final bool isSend = transactionModel.type == "send";
result[transactionModel.hash] = NanoTransactionInfo(
id: transactionModel.hash,
amountRaw: transactionModel.amount,
height: transactionModel.height,
direction: transactionModel.type == "send"
? TransactionDirection.outgoing
: TransactionDirection.incoming,
direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming,
confirmed: transactionModel.confirmed,
date: transactionModel.date ?? DateTime.now(),
confirmations: transactionModel.confirmed ? 1 : 0,
to: isSend ? transactionModel.account : address,
from: isSend ? address : transactionModel.account,
);
}
@ -312,11 +327,10 @@ abstract class NanoWalletBase
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _updateBalance();
await updateTransactions();
// setup a timer to receive transactions periodically:
_receiveTimer?.cancel();
_receiveTimer = Timer.periodic(const Duration(seconds: 15), (timer) async {
_receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async {
// get our balance:
await _updateBalance();
// if we have anything to receive, process it:
@ -325,6 +339,14 @@ abstract class NanoWalletBase
}
});
// also run once, immediately:
await _updateBalance();
bool updateSuccess = await updateTransactions();
if (!updateSuccess) {
syncStatus = FailedSyncStatus();
return;
}
syncStatus = SyncedSyncStatus();
} catch (e) {
print(e);
@ -353,9 +375,11 @@ abstract class NanoWalletBase
final data = json.decode(jsonSource) as Map;
final mnemonic = data['mnemonic'] as String;
final balance = NanoBalance.fromString(
formattedCurrentBalance: data['currentBalance'] as String? ?? "0",
formattedReceivableBalance: data['receivableBalance'] as String? ?? "0");
final balance = NanoBalance.fromRawString(
currentBalance: data['currentBalance'] as String? ?? "0",
receivableBalance: data['receivableBalance'] as String? ?? "0",
);
DerivationType derivationType = DerivationType.nano;
if (data['derivationType'] == "DerivationType.bip39") {
@ -374,12 +398,26 @@ abstract class NanoWalletBase
}
Future<void> _updateBalance() async {
var oldBalance = balance[currency];
try {
balance[currency] = await _client.getBalance(_publicAddress!);
} catch (e) {
print("Failed to get balance $e");
// if we don't have a balance, we should at least create one, since it's a late binding
// otherwise, it's better to just leave it as whatever it was before:
if (balance[currency] == null) {
balance[currency] =
NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero);
}
}
// don't save unnecessarily:
// trying to save too frequently can cause problems with the file system
// since nano is updated frequently this can be a problem, so we only save if there is a change:
if (oldBalance == null ||
balance[currency]!.currentBalance != oldBalance.currentBalance ||
balance[currency]!.receivableBalance != oldBalance.receivableBalance) {
await save();
}
await save();
}
Future<void> _updateRep() async {
@ -394,11 +432,19 @@ abstract class NanoWalletBase
}
Future<void> regenerateAddress() async {
final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd";
_privateKey =
await NanoUtil.uniSeedToPrivate(_hexSeed!, this.walletAddresses.account!.id, type);
_publicAddress =
await NanoUtil.uniSeedToAddress(_hexSeed!, this.walletAddresses.account!.id, type);
final NanoDerivationType type = (_derivationType == DerivationType.nano)
? NanoDerivationType.STANDARD
: NanoDerivationType.HD;
_privateKey = await NanoDerivations.universalSeedToPrivate(
_hexSeed!,
index: this.walletAddresses.account!.id,
type: type,
);
_publicAddress = await NanoDerivations.universalSeedToAddress(
_hexSeed!,
index: this.walletAddresses.account!.id,
type: type,
);
this.walletInfo.address = _publicAddress!;
this.walletAddresses.address = _publicAddress!;

View file

@ -6,12 +6,12 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_nano/nano_mnemonic.dart' as nm;
import 'package:cw_nano/nano_util.dart';
import 'package:cw_nano/nano_wallet.dart';
import 'package:cw_nano/nano_wallet_creation_credentials.dart';
import 'package:hive/hive.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:nanodart/nanodart.dart';
import 'package:nanoutil/nanoutil.dart';
class NanoWalletService extends WalletService<NanoNewWalletCredentials,
NanoRestoreWalletFromSeedCredentials, NanoRestoreWalletFromKeysCredentials> {
@ -30,7 +30,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
// nano standard:
DerivationType derivationType = DerivationType.nano;
String seedKey = NanoSeeds.generateSeed();
String mnemonic = NanoUtil.seedToMnemonic(seedKey);
String mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey);
credentials.walletInfo!.derivationType = derivationType;
@ -95,7 +95,7 @@ class NanoWalletService extends WalletService<NanoNewWalletCredentials,
// we can't derive the mnemonic from the key in all cases, only if it's a "nano" seed
if (credentials.seedKey.length == 64) {
try {
mnemonic = NanoUtil.seedToMnemonic(credentials.seedKey);
mnemonic = NanoDerivations.standardSeedToMnemonic(credentials.seedKey);
} catch (e) {
throw Exception("Wasn't a valid nano style seed!");
}

View file

@ -1,6 +1,6 @@
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_nano/nano_client.dart';
import 'package:cw_nano/nano_util.dart';
import 'package:nanoutil/nanoutil.dart';
class PendingNanoTransaction with PendingTransaction {
PendingNanoTransaction({
@ -18,13 +18,13 @@ class PendingNanoTransaction with PendingTransaction {
@override
String get amountFormatted {
final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano);
final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
return amt;
}
String get accurateAmountFormatted {
final String amt = NanoUtil.getRawAsUsableString(amount.toString(), NanoUtil.rawPerNano);
final String acc = NanoUtil.getRawAccuracy(amount.toString(), NanoUtil.rawPerNano);
final String amt = NanoAmounts.getRawAsUsableString(amount.toString(), NanoAmounts.rawPerNano);
final String acc = NanoAmounts.getRawAccuracy(amount.toString(), NanoAmounts.rawPerNano);
return "$acc$amt";
}

View file

@ -22,6 +22,10 @@ dependencies:
hex: ^0.2.0
http: ^1.1.0
shared_preferences: ^2.0.15
nanoutil:
git:
url: https://github.com/perishllc/nanoutil.git
ref: c37e72817cf0a28162f43124f79661d6c8e0098f
cw_core:
path: ../cw_core